#!/usr/bin/ruby
#
# Copyright:: Copyright 2013-2019, Trimble Inc.
# License:: All Rights Reserved.
# Original Author:: Scott Lininger (mailto:scott@sketchup.com)
#
# This file declares a few new methods on the following Ruby API classes:
#
#     Geom::Transformation          Methods for getting rotation and scaling
#     Sketchup::ComponentInstance   Method for making a copy of an instance
#     Sketchup::Drawingelement      Methods for getting sizes and recent scale
#
# Finally and most importantly, it declares the DynamicComponents class
# that contains the assorted methods to redraw smart components and provide
# UI for manipulating them.
#
#     DynamicComponents          Redraws and shows UI
#
# IMPORTANT NOTE: For SU 2015, we introduced a CDynamicComponentInfo C++ class
# in the apihelpers project that can retrieve information about a dynamic
# component and its attributes.  This is intended to be used to expose
# dynamic component information via our C/C++ APIs.  The CDynamicComponentInfo
# class is heavily dependent upon how this DynamicComponents class stores
# dynamic component attributes using SU attributes and how it computes the
# display value of attributes based on their units settings.  From now on,
# it's critical that we keep this code in sync with CDynamicComponentInfo!

require "sketchup.rb"
require "langhandler.rb"
Sketchup::require "su_dynamiccomponents/ruby/dcfunctions_v1"
Sketchup::require "su_dynamiccomponents/ruby/dcclassifier"

################################################################################
# The DynamicComponents class. An instance of this class handles all of the
# DC redraws and presents UI for managing and configuring DCs.
class DynamicComponentsV1

  # The maximum length for a component or group name that we will allow.
  MAX_ENTITY_NAME_LENGTH = 64

  # Default dialog sizes, maximums and minimums
  DEFAULT_OPTIONS_WIDTH = 230
  DEFAULT_OPTIONS_HEIGHT = 300
  DEFAULT_OPTIONS_TOP = 100
  DEFAULT_OPTIONS_LEFT = 400

  DEFAULT_ATTRIBUTES_WIDTH = 400
  DEFAULT_ATTRIBUTES_HEIGHT = 600
  DEFAULT_ATTRIBUTES_TOP = 100
  DEFAULT_ATTRIBUTES_LEFT = 100

  DEFAULT_REPORT_WIDTH = 1024
  DEFAULT_REPORT_HEIGHT = 668
  DEFAULT_REPORT_TOP = 50
  DEFAULT_REPORT_LEFT = 50

  MIN_REPORT_WIDTH = 300
  MIN_REPORT_HEIGHT = 200
  MAX_REPORT_WIDTH = 1024
  MAX_REPORT_HEIGHT = 768

  # Initialize the DynamicComponents object. Creates several instance variables
  # that are used to maintain state and provide some library data that
  # determines where certain attributes are stored and how they behave.
  #
  #  Args:
  #     None.
  def initialize(observers_instance)
    # puts "DynamicComponentsV1 initialize   ……ing👇"
    # Store a pointer back to our observers instance so we can get a few global
    # state variables.
    @dcobservers = observers_instance

    # These variables identify which file format version this extension will
    # create. Every component edited with this version of the plugin will be
    # tagged with this format inside an attribute called _formatversion
    @format_version = 1.0

    if Object::RUBY_PLATFORM =~ /darwin/i
      @is_mac = true
    else
      @is_mac = false
    end

    # This is the attribute dictionary name where all default "dynamic"
    # attributes will be stored.
    @dictionary_name = "dynamic_attributes"

    @dialogs = {}
    # puts "@dialogs 👉 #{@dialogs}"
    @animation_list = []
    @nudge_active_view_up = true

    # This counter is used in push/pull operations to ensure the best
    # behavior with start & commit operations.
    @dialogs_to_refresh = 0

    # Generate a list of the standard colors that we can use for material
    # assignments so we don't have to do it every time one is assigned
    @color_list = []
    color_array = Sketchup::Color.names
    for color_name in color_array
      @color_list.push color_name.downcase
    end

    # This @parser_functions varible could point to other versions of the
    # DCFunctions class as the library of functions is extended.
    @parser_functions = DCFunctionsV1.new

    # This is our converter tool kit.
    @conv = DCConverter.new

    # store a hash of entity pointers so we can get to them by ID later
    @entity_pointers = {}

    # Create some hashes that we'll cache formula results in to improve
    # performance.
    @formula_results = {}
    @formula_errors = {}

    # If the author has exposed even a "live value" attribute to be editable
    # via a text box, then the values entered by an end user will be stored
    # in this @forced_config_values hash. This hash takes precedence over
    # any value or formula for the current redraw cycle. This hash will be
    # emptied at the end of the redraw() method.
    @forced_config_values = {}

    # Because instances never come in with a material, we use this hash to
    # store the last applied material name in cases where we've just started
    # a place component and need to apply the correct material as soon as the
    # user finishes placing the component.
    @forced_material = {}

    # Certain attributes are attached to component instances instead of the
    # definition. This hash will contain a true value for each of these
    # "instance attached" attributes.
    # IMPORTANT NOTE: Only the value of the attribute is stored at the instance
    # level. The formulas, labels, units, etc. etc. are all stored at the
    # definition level.
    @instance_attributes = Hash[
      "x", true,
      "y", true,
      "z", true,
      "rotx", true,
      "roty", true,
      "rotz", true,
      "copy", true,
      "copies", true,
      "_iscollapsed", true
    ]

    # Store some default sort order values for common attributes
    @default_sorts = Hash[
    # METADATA
      "name", 10,
      "summary", 20,
      "description", 30,
      "itemcode", 50,
      # SIZE AND POSITION
      "x", 70,
      "y", 80,
      "z", 90,
      "lenx", 100,
      "leny", 110,
      "lenz", 120,
      "rotx", 130,
      "roty", 140,
      "rotz", 150,
      # BEHAVIORS
      "material", 160,
      "scaletool", 175,
      "hidden", 180,
      "onclick", 190,
      "copies", 200,
      # FORM DESIGN
      "imageurl", 210,
      "dialogwidth", 230,
      "dialogheight", 240
    ]

    # This is the "prefix" that we will attach to definition-level attributes
    # that are usually stored on the instance. This is a way of caching
    # current instance-level attributes onto the definition so they are
    # preserved during a Right Click > Save As. Example attribute name might
    # be "_inst__x_formula" or "_inst_z".
    @instance_cache_prefix = "_inst_"

    # Flag indicating whether the Classifier attributes should be simplified.
    @simplify = true
  end

  # Shows a WebDialog after creating it (if needed)
  #
  #    Args:
  #      dialog_name: required string, a unique, friendly name for the dialog
  #      dialog_url: optional string, the url to display
  #      dialog_w: optional int, width of the dialog in pixels
  #      dialog_h: optional int, height of the dialog in pixels
  #      dialog_x: optional int, screen x position of the dialog in pixels
  #      dialog_y: optional int, screen y position of the dialog in pixels
  #      is_modal: optional boolean, whether to show as a modal dialog
  #      clear_highlight_on_close: optional boolean, whether to clear our
  #        "highlight", which is a component that is created inside components
  #        to indicate that they are selected inside the manager. If the manager
  #        is closed by the user, then we handle that event if this is TRUE.
  def show_dialog(dialog_name, dialog_url = "http://www.sketchup.com",
                               dialog_w = DEFAULT_REPORT_WIDTH, dialog_h = DEFAULT_REPORT_HEIGHT,
                               dialog_x = DEFAULT_REPORT_LEFT, dialog_y = DEFAULT_REPORT_TOP, is_modal = false,
                               clear_highlight_on_close = false)
    puts "show_dialog   ……ing👇"
    puts "dialog_name 👉 #{dialog_name}"
    # Apply a minimum size to these dialogs.
    if dialog_w < MIN_REPORT_WIDTH
      dialog_w = MIN_REPORT_WIDTH
    end
    if dialog_w > MAX_REPORT_WIDTH
      dialog_w = MAX_REPORT_WIDTH
    end
    if dialog_h < MIN_REPORT_HEIGHT
      dialog_h = MIN_REPORT_HEIGHT
    end
    if dialog_h > MAX_REPORT_HEIGHT
      dialog_h = MAX_REPORT_HEIGHT
    end

    # Check to see if this dialog has already been created. If not, create it.
    puts "@dialogs[dialog_name] 👉 #{@dialogs[dialog_name]}"
    if @dialogs[dialog_name] == nil
      puts "重新创建   ……ing👇"
      # The reporter disallows resizing.
      is_reporter = dialog_name == $dc_REPORTER_NAME
      @dialogs[dialog_name] = UI::WebDialog.new(translate(dialog_name),
                                                false, dialog_name, dialog_w, dialog_h,
                                                dialog_x, dialog_y, !is_reporter)

      # Activate full security mode, which disallows navigation to sites
      # outside of sketchup.google.com.
      # Note(tyler): removed fully security as it was blocking testing
      # and is not something we truly need.
      # @dialogs[dialog_name].set_full_security()

      # The manager panel needs to clear up the highlight instance on close.
      if clear_highlight_on_close == true
        @dialogs[dialog_name].set_on_close { destroy_highlight_instance() }
      end

      @dialogs[dialog_name].add_action_callback("pull_attribute_tree") do |d, p|
        pull_attribute_tree(d, p)
      end
      @dialogs[dialog_name].add_action_callback("pull_translation") do |d, p|
        pull_translation(d, p)
      end
      @dialogs[dialog_name].add_action_callback("pull_live_value") do |d, p|
        pull_live_value(d, p)
      end
      @dialogs[dialog_name].add_action_callback("pull_format_length") do |d, p|
        pull_format_length(d, p)
      end
      @dialogs[dialog_name].add_action_callback("do_close") do |d, p|
        do_close(d, p)
      end
      @dialogs[dialog_name].add_action_callback("do_set_simplify") do |d, p|
        do_set_simplify(d, p)
      end
      @dialogs[dialog_name].add_action_callback("do_delete_attribute") do |d, p|
        do_delete_attribute(d, p)
      end
      @dialogs[dialog_name].add_action_callback("push_attribute") do |d, p|
        push_attribute(d, p)
      end
      @dialogs[dialog_name].add_action_callback("push_rename") do |d, p|
        push_rename(d, p)
      end
      @dialogs[dialog_name].add_action_callback("push_selection") do |d, p|
        push_selection(d, p)
      end
      @dialogs[dialog_name].add_action_callback("do_redraw") do |d, p|
        do_redraw(d, p)
      end
      @dialogs[dialog_name].add_action_callback("do_open_url") do |d, p|
        do_open_url(d, p)
      end
      @dialogs[dialog_name].add_action_callback("do_show_dialog") do |d, p|
        do_show_dialog(d, p)
      end
      @dialogs[dialog_name].add_action_callback("do_send_action") do |d, p|
        do_send_action(d, p)
      end
      @dialogs[dialog_name].add_action_callback("js_log") do |d, p|
        js_log(d, p)
      end
      @dialogs[dialog_name].add_action_callback("pull_translations") do |d, p|
        pull_translations(d, p)
      end
      @dialogs[dialog_name].add_action_callback("do_place_component") do |d, p|
        do_place_component(d, p)
      end
      @dialogs[dialog_name].add_action_callback("do_load_from_url") do |d, p|
        do_load_from_url(d, p)
      end
    end

    # This is a way to force a reload of a page, by seting its url to nothing
    # and then setting it to something else in a moment...
    @dialogs[dialog_name].set_url("")

    # Set the size of our dialog, and remember it so future resizes can be
    # ignored if there's not actually a resize. We do this because resizing
    # causes the dialogs to steal focus on the PC, so we only want to do it
    # when the size is actually changing. (See set_dialog_properties method
    # for more information.)
    @dialogs[dialog_name].set_size(dialog_w, dialog_h)
    @dialogs[dialog_name].last_width = dialog_w
    @dialogs[dialog_name].last_height = dialog_h

    @dialogs[dialog_name].min_width = MIN_REPORT_WIDTH
    @dialogs[dialog_name].min_height = MIN_REPORT_HEIGHT

    # If there is no 'http:' or 'https:' in the url, calculate a relative path
    if dialog_url.index(/https*:/) != 0
      # Do a search for the file we want to display
      path_array = dialog_url.split(/\//)
      file_to_find = path_array.pop
      puts "file_to_find👉 #{file_to_find}"
      path_to_search = path_array.join("/")
      puts "path_to_search 👉 #{path_to_search}"
      # found_file = Sketchup.find_support_file file_to_find, path_to_search
      found_file = File.join(path_to_search, file_to_find)
      @dialogs[dialog_name].set_file(found_file)
    else
      @dialogs[dialog_name].set_url dialog_url
    end
    @dialogs[dialog_name].set_background_color(
      @dialogs[dialog_name].get_default_dialog_color
    )
    if is_modal == true || @is_mac == true
      @dialogs[dialog_name].show_modal
    else
      @dialogs[dialog_name].show
    end
  end

  # Pops open the configure dialog for a given selection
  #
  #   Args:
  #     None.
  #
  #   Returns:
  #     Nothing
  def show_configure_dialog()
    dialog_w = DEFAULT_OPTIONS_WIDTH
    dialog_h = DEFAULT_OPTIONS_HEIGHT

    if Sketchup.active_model.selection.length == 1
      # TODO(scottlininger): This whole method will need to be remade to do
      # the work of deciding the width and height of the dialog from inside
      # pull_attribute_tree and the javascript, since we'll need to determine
      # the dialog size that is appropriate across multiple selections.
      #
      # For now, if only one entity is selected, then use that as our configure
      # item. But this whole block will likely go away once the multiselect
      # configure feature is implemented.
      entity = Sketchup.active_model.selection[0]
      entity = explode_if_3d_warehouse(entity)
      save_configure_thumbnail()
      if get_attribute_value(entity, "dialogwidth") != nil
        dialog_w = get_attribute_value(entity, "dialogwidth").to_i
      end
      if get_attribute_value(entity, "dialogheight") != nil
        dialog_h = get_attribute_value(entity, "dialogheight").to_i
      end
    end

    show_dialog($dc_CONFIGURATOR_NAME,
                File.join($Ahtml目录, "configurator.html"),
                dialog_w, dialog_h, DEFAULT_OPTIONS_LEFT, DEFAULT_OPTIONS_TOP, false)
  end

  # Pops open the attribute manager dialog for a given component
  #
  #   Args:
  #     entity: reference to the dynamic component to be configured
  #
  #   Returns:
  #     Nothing
  def show_manager_dialog(entity)
    # Do not allow free users to call this manually with the ruby console.
    if Sketchup.is_pro? == false
      return
    end
    entity = explode_if_3d_warehouse(entity)
    @last_selected_entity = entity
    @entity_pointers[entity.entityID.to_s] = entity
    show_dialog($dc_MANAGER_NAME,
                File.join($Ahtml目录, "manager.html"),
                DEFAULT_ATTRIBUTES_WIDTH, DEFAULT_ATTRIBUTES_HEIGHT,
                DEFAULT_ATTRIBUTES_LEFT, DEFAULT_ATTRIBUTES_TOP, false, true)
  end

  # Pops open the reports dialog
  #
  #   Args:
  #     None
  #
  #   Returns:
  #     Nothing
  def show_reporter_dialog()
    if Sketchup.is_online == true
      url = Sketchup.get_datfile_info "GENERATE_REPORT",
                                      "https://3dwarehouse.sketchup.com/generatereport.html"
    else
      url = File.join($Ahtml目录, "reporter.html")
    end
    # Show the reporter panel as a non-modal window. (There is a bug somehow in
    # su.init() where modal windows are not pulling in the information
    # correctly, so the easy solution is to go non-modal.)
    show_dialog($dc_REPORTER_NAME, url,
                DEFAULT_REPORT_WIDTH, DEFAULT_REPORT_HEIGHT,
                DEFAULT_REPORT_LEFT, DEFAULT_REPORT_TOP, false)
  end

  # Redraws the given dynamic component with its current size, position, and
  # attribute values.
  #
  #   Args:
  #     entity: reference to the dynamic component to be configured
  #
  #   Returns:
  #     Nothing
  def redraw_with_undo(entity, progress_bar_visible = true)
    Sketchup.active_model.start_operation translate("Redraw"), true
    determine_movetool_behaviors(entity)
    DCProgressBar::clear()
    redraw(entity)
    DCProgressBar::clear()
    refresh_dialogs()
    Sketchup.active_model.commit_operation
  end

  # Redraws the given dynamic component with its current size, position, and
  # attribute values.
  #
  #   Args:
  #                 entity: reference to the dynamic component to be configured
  #   progress_bar_visible: boolean that contains TRUE if we should update the
  #                         progress bar as we redraw
  #      is_recursive_call: boolean containing TRUE if redraw is being called
  #                         recursively from within a previous redraw call.
  #
  #   Returns:
  #     boolean:            False if the user has cancelled the redraw via
  #                         the progress bar cancel dialog. True otherwise.
  def redraw(entity, progress_bar_visible = true, is_recursive_call = false)

    # If we're redrawing at the top level of a tree, then clear out our previous
    # formula results. These hashes will cache results so that subsequent
    # formula parsing is faster.
    if is_recursive_call == false
      @formula_results = {}
      @formula_errors = {}

      # Mark the current time so we can calculate redraw speed.
      start_time = Time.new

      # Recheck for DCness so we always update the DC badge on redraw.
      has_behaviors(entity)
    end

    if entity.is_a?(Sketchup::ComponentInstance)
      entity = explode_if_3d_warehouse(entity)
      make_unique_if_needed(entity)

      # Look for any "cached" values from a right click > save as and apply them
      # to this component.
      subentities = entity.definition.entities
    else
      subentities = entity.entities
    end

    # Move into correct coordinate space, regardless of edit mode.
    local_origin_transform = entity.local_transformation
    entity.transform! local_origin_transform.inverse

    # Store the current translation.
    new_x = entity.transformation.origin.x.to_f
    new_y = entity.transformation.origin.y.to_f
    new_z = entity.transformation.origin.z.to_f

    # Calculate the new translation.
    target_x = get_formula_result(entity, "x")
    target_y = get_formula_result(entity, "y")
    target_z = get_formula_result(entity, "z")

    definition_origin = Geom::Point3d.new(0, 0, 0)

    # components with subentities that have behaviors never actually scale
    # themselves. They instead apply their scale directly to the subentities
    if children_have_behaviors(entity)
      visual_lenx, visual_leny, visual_lenz = entity.scaled_size
      xscale, yscale, zscale = entity.last_scaling_factors

      # reset the entity's scale
      scale_transform = Geom::Transformation.scaling definition_origin,
                                                     1.0 / entity.transformation.xscale,
                                                     1.0 / entity.transformation.yscale,
                                                     1.0 / entity.transformation.zscale
      entity.transformation = entity.transformation * scale_transform

      # Store our unscaled bounding box so we can properly determine whether
      # to apply changes to our nominal attribute values.
      start_lenx, start_leny, start_lenz = entity.unscaled_size

      # Since we cannot rely on the bounding box to give us the "size" of
      # a component that can contain parts that jut out beyond the bounding box
      # we need to apply the scaling factor to our nominal size and store that.
      # The method get_attribute_value() will return us the entity's
      # nominal size if it has been previously set. Otherwise it will return
      # the "live value" of the attribute, which would be its scaled size
      nominal_lenx = get_attribute_value(entity, "lenx").to_f
      nominal_leny = get_attribute_value(entity, "leny").to_f
      nominal_lenz = get_attribute_value(entity, "lenz").to_f

      # With totally flat parts, the nominal size can be reported by the API as
      # NaN, which makes subsequent calculations impossible. Therefore, we need
      # to default to 0.0.
      nominal_lenx = second_if_nan(nominal_lenx, 0.0)
      nominal_leny = second_if_nan(nominal_leny, 0.0)
      nominal_lenz = second_if_nan(nominal_lenz, 0.0)

      new_lenx = nominal_lenx * xscale
      new_leny = nominal_leny * yscale
      new_lenz = nominal_lenz * zscale

      # Let's store our assumed nominal size, so that the current() function has
      # access to the size that the user is trying to apply
      store_nominal_size(entity, new_lenx, new_leny, new_lenz)

      target_lenx = second_if_empty(get_formula_result(entity, "lenx"),
                                    new_lenx).to_f
      target_leny = second_if_empty(get_formula_result(entity, "leny"),
                                    new_leny).to_f
      target_lenz = second_if_empty(get_formula_result(entity, "lenz"),
                                    new_lenz).to_f

      # Now store our calculated size into the nominal attributes so that
      # we have access to them the next time this component is scaled. (For
      # example, we may have just calculated the width to be 50", but some
      # sub-part is soon going to position itself at x=60. If we didn't
      # store the fact that on our last scale the component set itself with
      # a size of 50", then our subsequent redraws would create strange
      # scaling results.
      store_nominal_size(entity, target_lenx, target_leny, target_lenz)

      # run all formulas and store their results into the attributes
      run_all_formulas(entity)

      # TODO(scottlininger): The following block should be abstracted into a
      # method, since it's used twice...
      target_rotx = get_formula_result(entity, "rotx")
      target_roty = get_formula_result(entity, "roty")
      target_rotz = get_formula_result(entity, "rotz")
      if target_rotx != nil || target_roty != nil || target_rotz != nil
        target_rotx = second_if_empty(target_rotx, 0.0).to_f
        target_roty = second_if_empty(target_roty, 0.0).to_f
        target_rotz = second_if_empty(target_rotz, 0.0).to_f
        entity.transform! entity.transformation.inverse
        if entity.transformation.xaxis.length > 0
          radians = target_rotx * (Math::PI / 180)
          new_rotx = Geom::Transformation.rotation entity.transformation.origin,
                                                   entity.transformation.xaxis, radians
          entity.transform! new_rotx
        end
        if entity.transformation.yaxis.length > 0
          radians = target_roty * (Math::PI / 180)
          new_roty = Geom::Transformation.rotation entity.transformation.origin,
                                                   entity.transformation.yaxis, radians
          entity.transform! new_roty
        end
        if entity.transformation.zaxis.length > 0
          radians = target_rotz * (Math::PI / 180)
          new_rotz = Geom::Transformation.rotation entity.transformation.origin,
                                                   entity.transformation.zaxis, radians
          entity.transform! new_rotz
        end
      end

      # If one of our sizes is 0, then our scaling factor will always be 1.0.
      if fix_float(nominal_lenx) == 0.0
        dlenx = 1.0
      else
        dlenx = target_lenx / nominal_lenx
      end
      if fix_float(nominal_leny) == 0.0
        dleny = 1.0
      else
        dleny = target_leny / nominal_leny
      end
      if fix_float(nominal_lenz) == 0.0
        dlenz = 1.0
      else
        dlenz = (target_lenz / nominal_lenz)
      end

      # SketchUp does not allow scales of true 0, so substitute a small scale.
      if dlenx == 0.0
        dlenx = 0.001
      end
      if dleny == 0.0
        dleny = 0.001
      end
      if dlenz == 0.0
        dlenz = 0.001
      end

      subentity_transform = Geom::Transformation.scaling definition_origin,
                                                         dlenx, dleny, dlenz

      # Loop across all subentities to apply the subentity_transform to
      # each of them.
      if dlenx != 1.0 || dleny != 1.0 || dlenz != 1.0

        # Keep a list of faces and edges
        naked_entities = []

        for subentity in subentities
          if subentity.is_a?(Sketchup::ComponentInstance) ||
             subentity.is_a?(Sketchup::Group)
            subentity.transform! subentity_transform
          elsif subentity.is_a?(Sketchup::Face) || subentity.is_a?(Sketchup::Edge)
            naked_entities.push subentity
          end
        end

        # Transform our naked entities all at once to avoid geometry corruption.
        if naked_entities.length > 0
          subentities.transform_entities subentity_transform, naked_entities

          # Now that we've rescaled our faces, rerun any formulas that use the
          # facearea() spreadsheet function.
          run_facearea_formulas(entity)
        end
      end

      # The following is a way to "force" update of the Bounding Box object in
      # the ruby API.
      if entity.is_a?(Sketchup::ComponentInstance)
        entity.definition.invalidate_bounds
      end
    else
      new_lenx, new_leny, new_lenz = entity.scaled_size

      # run all formulas and store their results into the attributes
      run_all_formulas(entity)

      unscaled_lenx, unscaled_leny, unscaled_lenz = entity.unscaled_size
      target_lenx = second_if_empty(get_formula_result(entity, "lenx"),
                                    new_lenx).to_f
      target_leny = second_if_empty(get_formula_result(entity, "leny"),
                                    new_leny).to_f
      target_lenz = second_if_empty(get_formula_result(entity, "lenz"),
                                    new_lenz).to_f

      # TODO(scottlininger): The following block should be abstracted into a
      # method, since it's used twice...
      target_rotx = get_formula_result(entity, "rotx")
      target_roty = get_formula_result(entity, "roty")
      target_rotz = get_formula_result(entity, "rotz")
      if target_rotx != nil || target_roty != nil || target_rotz != nil
        target_rotx = second_if_empty(target_rotx, 0.0).to_f
        target_roty = second_if_empty(target_roty, 0.0).to_f
        target_rotz = second_if_empty(target_rotz, 0.0).to_f
        entity.transform! entity.transformation.inverse
        if entity.transformation.xaxis.length > 0
          radians = target_rotx * (Math::PI / 180)
          new_rotx = Geom::Transformation.rotation entity.transformation.origin,
                                                   entity.transformation.xaxis, radians
          entity.transform! new_rotx
        end
        if entity.transformation.yaxis.length > 0
          radians = target_roty * (Math::PI / 180)
          new_roty = Geom::Transformation.rotation entity.transformation.origin,
                                                   entity.transformation.yaxis, radians
          entity.transform! new_roty
        end
        if entity.transformation.zaxis.length > 0
          radians = target_rotz * (Math::PI / 180)
          new_rotz = Geom::Transformation.rotation entity.transformation.origin,
                                                   entity.transformation.zaxis, radians
          entity.transform! new_rotz
        end
      else
        # Undo the scale transform so we can get at our current size.
        scale_transform = Geom::Transformation.scaling definition_origin,
                                                       1.0 / entity.transformation.xscale,
                                                       1.0 / entity.transformation.yscale,
                                                       1.0 / entity.transformation.zscale
        entity.transformation = entity.transformation * scale_transform
      end
      if target_lenx != unscaled_lenx || target_leny != unscaled_leny || target_lenz != unscaled_lenz
        if fix_float(unscaled_lenx) == 0.0
          target_xs = 1.0
        else
          target_xs = (target_lenx / unscaled_lenx).abs
        end
        if fix_float(unscaled_lenz) == 0.0
          target_zs = 1.0
        else
          target_zs = (target_lenz / unscaled_lenz).abs
        end
        if fix_float(unscaled_leny) == 0.0
          target_ys = 1.0
        else
          target_ys = (target_leny / unscaled_leny).abs
        end
        origin = entity.transformation.origin
        # SketchUp does not allow scales of true 0, so substitute a small scale
        if target_xs == 0.0
          target_xs = 0.001
        end
        if target_ys == 0.0
          target_ys = 0.001
        end
        if target_zs == 0.0
          target_zs = 0.001
        end
        new_scale = Geom::Transformation.scaling origin,
                                                 target_xs, target_ys, target_zs
        entity.transformation = entity.transformation * new_scale
      end
    end

    # Now we need to move from the current position to our final position
    dx = second_if_empty(target_x, new_x).to_f -
         entity.transformation.origin.x.to_f
    dy = second_if_empty(target_y, new_y).to_f -
         entity.transformation.origin.y.to_f
    dz = second_if_empty(target_z, new_z).to_f -
         entity.transformation.origin.z.to_f

    translation_vector = Geom::Vector3d.new dx, dy, dz
    translation = Geom::Transformation.translation translation_vector
    entity.transform! translation

    # Now that our entity is completely transformed, we can move it back into
    # the "local" transformation that we stored at the beginning of the redraw.
    entity.transform! local_origin_transform

    # Determine if the Instance should be hidden based on the DC attributes.
    # Set its hidden status accordingly.
    # New for SketchUp 2020.0 added functionality that updates the instance's
    # visibility state in each scene as well. This was done because we have
    # introduced functionality in Scenes where they now remember the visibility
    # state of nested entities. This caused an breakage in DC's which had been
    # relying on Scenes NOT remembering their visibility state previously.

    target_hidden = get_attribute_value(entity, "hidden")

    if target_hidden != nil
      is_hidden = target_hidden.to_f > 0.0
      entity.hidden = is_hidden
      pages = Sketchup.active_model.pages
      if pages.length > 0 && pages[0].respond_to?(:set_drawingelement_visibility)
        pages.each do |page|
          page.set_drawingelement_visibility(entity, !is_hidden)
        end
      end
    end

    target_no_scale_mask = get_attribute_value(entity, "scaletool")
    if entity.is_a?(Sketchup::ComponentInstance)
      behavior = entity.definition.behavior
      if behavior.respond_to? :no_scale_mask?
        behavior.no_scale_mask = target_no_scale_mask.to_i
      end
    end

    target_material = second_if_empty(
      get_forced_config_value(entity, "material"),
      get_attribute_value(entity, "material")
    ).to_s

    if target_material != nil
      target_color = target_color.to_s
      if @color_list.include? target_material.downcase
        # If the material is on the standard color list, use that.
        entity.material = target_material
      elsif target_material.length > 0
        m = nil
        # Look through the in model materials next to see if it exists there.
        if Sketchup.active_model.materials[target_material]
          m = Sketchup.active_model.materials[target_material]
        elsif Sketchup.active_model.materials["[" + target_material + "]"]
          m = Sketchup.active_model.materials["[" + target_material + "]"]
        elsif Sketchup.active_model.materials["<" + target_material + ">"]
          m = Sketchup.active_model.materials["<" + target_material + ">"]
        end
        if m != nil
          entity.material = m
        elsif (target_material.gsub(/\s/, "").index(
          /\#{0,1}[abcdef\d]{3}$/i
        ) == 0)
          # This regexp above looks for a 3-digit hex value, such as #AAA or 0f0
          target_material = target_material.gsub(/\s/, "")
          if target_material[0..0] == "#"
            target_material = target_material[1..999]
          end
          r = target_material[0..0].to_i(16) * 16
          g = target_material[1..1].to_i(16) * 16
          b = target_material[2..2].to_i(16) * 16
          parsed_color = Sketchup::Color.new r, g, b
          new_material = Sketchup.active_model.materials.add target_material
          new_material.color = parsed_color
          entity.material = new_material
        elsif (target_material.gsub(/\s/, "").index(
          /\#{0,1}[abcdef\d]{6}$/i
        ) == 0)
          # This regexp above looks for a hex value, such as #A0BBEC or 00ff00
          target_material = target_material.gsub(/\s/, "")
          if target_material[0..0] == "#"
            target_material = target_material[1..999]
          end
          r = target_material[0..1].to_i(16)
          g = target_material[2..3].to_i(16)
          b = target_material[4..5].to_i(16)
          parsed_color = Sketchup::Color.new r, g, b
          new_material = Sketchup.active_model.materials.add target_material
          new_material.color = parsed_color
          entity.material = new_material
        elsif (target_material.index(
          /\s*\d{1,3}\.?\d*\s*,\s*\d{1,3}\.?\d*\s*,\s*\d{1,3}\.?\d*\s*$/i
        ) == 0)
          # This regexp above looks 3-number RGB value, ignoring whitespace,
          # such as 125,0,0 or 255, 255, 255
          r, g, b = target_material.gsub(/\s/, "").split(",")
          r = r.to_i
          g = g.to_i
          b = b.to_i
          parsed_color = Sketchup::Color.new r, g, b
          new_material = Sketchup.active_model.materials.add target_material
          new_material.color = parsed_color
          entity.material = new_material
        else
          # If we haven't found a valid material, set an error so the user
          # has some indication of what is happening.
          set_attribute_error(entity, "material",
                              "<span class=subformula-error>" +
                              translate("Material not in model:") +
                              " " + target_material +
                              "</span>")
          entity.material = nil
        end
      end
    end

    # Clear out any set of forced config values. (See push_attribute_set for
    # comments on implementation.)
    new_lenx = second_if_empty(
      get_forced_config_value(entity, "lenx"),
      new_lenx
    )
    new_leny = second_if_empty(
      get_forced_config_value(entity, "leny"),
      new_leny
    )
    new_lenz = second_if_empty(
      get_forced_config_value(entity, "lenz"),
      new_lenz
    )
    clear_forced_config_values(entity)

    # handle copies
    if copy_number_of(entity) == 0.0
      target_copies = get_attribute_value(entity, "copies")
      if target_copies != nil && target_copies.to_s != "NaN"
        target_copies = target_copies.to_f

        # keep track of which copies exist and which need to be deleted
        existing_copies = {}
        entities_to_delete = []

        dc_name = get_attribute_value(entity, "_name")
        if dc_name == nil
          dc_name = entity.name
        end

        for sibling in entity.parent.entities
          if sibling.typename == entity.typename
            # if we've found a copy, see if it needs to be deleted
            # TODO(scottlininger): Look instead at attributes for matching IDs -- more robust
            if sibling.name.index(dc_name + " copy") == 0
              sibling_copy_number = copy_number_of(sibling)
              if sibling_copy_number > target_copies
                entities_to_delete.push sibling
              else
                existing_copies[sibling_copy_number.to_i] = sibling
              end
            end
          end
        end

        for entity_to_delete in entities_to_delete
          entity_to_delete.erase!
        end

        i = 1
        while i <= target_copies.to_i
          if existing_copies[i] != nil
            existing_copies[i].name = name_of_copy(dc_name, i)
            # copy the attributes down, in case they have changed
            clone_attributes(entity, existing_copies[i])
            set_attribute(existing_copies[i], "copy", i)
            existing_copies[i].material = entity.material
            success = redraw(existing_copies[i], progress_bar_visible, true)
          else
            # TODO(scottlininger): Only execute formulas with COPY in them for copied parts
            new_entity = entity.copy
            new_entity.name = name_of_copy(dc_name, i)
            clone_attributes(entity, new_entity)
            set_attribute(new_entity, "copy", i)
            new_entity.material = entity.material
            success = redraw(new_entity, progress_bar_visible, true)
          end
          if not success
            return false
          end
          i = i + 1
        end
      end
    end # handling copies

    for subentity in subentities
      if subentity.is_a?(Sketchup::ComponentInstance) ||
         subentity.is_a?(Sketchup::Group)
        if has_behaviors(subentity)
          if get_attribute_value(subentity, "copy").to_f < 1
            success = redraw(subentity, progress_bar_visible, true)
            if not success
              return false
            end
          end
        end
      end
    end

    # store the new bounding box size after the subparts have moved themselves
    # TODO(scottlininger): should be be recalling scaled_size at this point?
    # seems like we know this from above and could save a few cycles.
    if children_have_behaviors(entity)
      lenx, leny, lenz = entity.unscaled_size

      if lenx == start_lenx && xscale != 1.0
        new_lenx = lenx
      end
      if leny == start_leny && yscale != 1.0
        new_leny = leny
      end
      if lenz == start_lenz && zscale != 1.0
        new_lenz = lenz
      end
      store_nominal_size(entity, new_lenx, new_leny, new_lenz)
      entity.set_last_size(lenx, leny, lenz)
    end

    # Output the time required to redraw.
    if is_recursive_call == false
      end_time = Time.new
      total_time = end_time - start_time
      if progress_bar_visible
        refresh_thumbnails(entity)
        puts total_time.to_s + translate(" seconds to redraw")
      end
    end

    if not DCProgressBar.advance(progress_bar_visible)
      return false
    else
      return true
    end
  end

  # Stores the passed size into an entity's attributes
  #
  # For components with parts that move themselves, the bounding box
  # lenx, leny, and lenz are not reliable "live values." In such cases,
  # we look for a stored attributes called _ATTNAME_nominal, which stores
  # the nominal value that is reliable. This nominal value is essentially
  # the size that the user or the attribute formulas *tried* to scale something
  # to. Whether that remains the actual size (in terms of bounding box) often
  # depends on what the subparts do.
  #
  # After scaling a dynamic component which has self-positioning parts,
  # the redraw method calls set_nominal_size, which stores these
  # attributes.
  #
  # For example, if I scale a component to be 20" wide, the component
  # might redraw itself to be physically 30" wide. But for purposes of
  # formula calculation, I want to return 20" for my width, or extremely
  # weird scaling bugs appear.
  #
  #   Args:
  #     entity: reference to the dynamic component to be configured
  #     target_lenx: float, width of the entity
  #     target_leny: float, depth of the entity
  #     target_lenz: float, height of the entity
  #
  #   Returns:
  #     Nothing
  def store_nominal_size(entity, target_lenx, target_leny, target_lenz)
    if entity.is_a?(Sketchup::Group)
      attribute_entity = entity
    else
      attribute_entity = entity.definition
    end

    attribute_entity.set_attribute @dictionary_name, "_lenx_nominal",
      target_lenx
    attribute_entity.set_attribute @dictionary_name, "_leny_nominal",
      target_leny
    attribute_entity.set_attribute @dictionary_name, "_lenz_nominal",
      target_lenz
  end

  # Clones all attributes from one entity to another. Used specifically for
  # copying attributes into "copies" of repeating parts
  #
  #   Args:
  #     source_entity: reference to the entity we're copying *from*
  #     target_entity: reference to the entity we're copying *to*
  #
  #   Returns:
  #     Nothing
  def clone_attributes(source_entity, target_entity)
    # TODO(scottlininger): should clean out old attributes in case some have
    # been deleted. Also, would this better belong as an extended method to API?
    #
    # Note: the true param in get_attribute_list tells us to get all attributes,
    # even "hidden" ones that begin with an underscore. This is because we need
    # to clone the _name attribute so self-referencial formulas work for copies.
    list = get_attribute_list(source_entity, true)
    for name in list
      value = get_attribute_value(source_entity, name)
      formula = get_attribute_formula(source_entity, name)
      label = get_attribute_label(source_entity, name)
      set_attribute(target_entity, name, value, formula, label)
    end
  end

  # Returns the copy number of a given entity, based on its name. The original
  # entity will return copy number 0.0, the first copy will return copy number
  # 1.0, and so on.
  #
  #   Args:
  #     entity: reference to the entity we want the copy number of
  #
  #   Returns:
  #     float: copy number of the entity
  def copy_number_of(entity)
    if entity.name =~ /copy \d+$/
      copy = entity.name[entity.name.index("copy ") + 5..999].to_f
      return copy
    else
      return 0.0
    end
  end

  # Returns true or false based on whether an entity has "behaviors" that could
  # require a redraw of that entity's geometry, position, or appearance.
  #
  # First, it looks for an attribute called _hasbehaviors. If that doesn't exist
  # then it will check for certain attributes to see if they are formula-driven
  # or even if they just have a value. If behaviors are found, it attaches
  # _hasbehaviors with a value of 1.0 so we don't have to reprocess that in
  # the future.
  #
  # This is a recursive method that calls itself for all child entities that
  # are groups or component instances. By definition, if an entity's children
  # have behaviors, then that entity has behaviors.
  #
  #   Args:
  #     entity: reference to the entity we want to check for behaviors
  #
  #   Returns:
  #     boolean: true if behaviors are present
  def has_behaviors(entity, force_refresh = false)

    # Check to see if we should recalculate our behaviors. If so, wipe out
    # our cached attribute so we will look for it fresh.
    if force_refresh == true
      delete_attribute(entity, "_hasbehaviors")
    end

    if entity == nil
      return false
    elsif entity == false
      return false
    elsif !entity.is_a?(Sketchup::ComponentInstance) && !entity.is_a?(Sketchup::Group)
      return false
    elsif get_attribute_value(entity, "_hasbehaviors") != nil
      return true
    elsif get_attribute_value(entity, "copies") != nil ||
          get_attribute_value(entity, "msrp") != nil ||
          get_attribute_value(entity, "onclick") != nil ||
          get_attribute_value(entity, "scaletool").to_i != 0
      set_attribute(entity, "_hasbehaviors", 1.0)
      return true
    else
      # Look at all attributes (including custom ones) for formulas or for
      # an access level that makes it visible in the configurator dialog.
      for key in get_attribute_list(entity)
        if get_attribute_formula(entity, key) != nil ||
           (get_attribute_access(entity, key).to_s != "NONE" &&
            get_attribute_access(entity, key).to_s != "")
          set_attribute(entity, "_hasbehaviors", 1.0)
          return true
        end
      end

      if entity.is_a?(Sketchup::ComponentInstance)
        subentities = entity.definition.entities
      else
        subentities = entity.entities
      end
      for subentity in subentities
        if subentity.is_a?(Sketchup::ComponentInstance) ||
           subentity.is_a?(Sketchup::Group)
          if has_behaviors(subentity, force_refresh) == true
            # TODO(scottlininger): There is a functional difference between
            # "having behaviors" and having the _hasbehaviors flag set to true.
            # Since the _hasbehaviors flag is what we use to drive the DC
            # badge in the component browser, we only want to set that
            # flag on components that are *themselves* DCs, not on components
            # that *contain* DCs. Therefore, this is the one branch where we
            # return true for the method but do NOT set the flag to 1.0.
            return true
          end
        end
      end
    end

    # If we are pushing down any configuration values at this point,
    # then return true to require a redraw.
    if has_forced_config_values(entity)
      return true
    end
    return false
  end

  # Returns true or false based on whether an entity has "movetool behaviors"
  # that could require a redraw of that entity based on something that changed
  # do to end user's use of the move tool.
  #
  # First, it looks for an attribute called _has_movetool_behaviors. If that
  # doesn't exist then it will call determine_movetool_behaviors() to get
  # its answer.
  #
  #   Args:
  #     entity: reference to the entity we want to check for behaviors
  #
  #   Returns:
  #     boolean: true if movetool behaviors are present
  def has_movetool_behaviors(entity)
    if entity == nil
      return false
    elsif entity == false
      return false
    elsif !entity.is_a?(Sketchup::ComponentInstance) &&
          !entity.is_a?(Sketchup::Group)
      return false
    elsif has_dc_attribute_dictionary(entity) == false
      return false
    elsif get_attribute_value(entity, "_has_movetool_behaviors") == 1.0
      return true
    elsif get_attribute_value(entity, "_has_movetool_behaviors") == 0.0
      return false
    else
      return determine_movetool_behaviors(entity)
    end
  end

  # Determines whether an entity has "movetool behaviors" that could require
  # a redraw of that entity based on something that changed do to end user's
  # use of the move tool.
  #
  # It will check all attribute formula of the entity and its children to see
  # if they refer to the attribute X,Y,Z or ROTX,ROTY,ROTZ. If behaviors are
  # found, it attaches _has_movetool_behaviors with a value of 1.0 so we don't
  # have to reprocess that in the future.
  #
  # This is a recursive method that calls itself for all child entities that
  # are groups or component instances. By definition, if an entity's children
  # have behaviors, then that entity has behaviors.
  #
  #   Args:
  #     entity: reference to the entity we want to check for behaviors
  #
  #   Returns:
  #     boolean: true if movetool behaviors are present
  def determine_movetool_behaviors(entity)
    if entity == nil
      return false
    elsif entity == false
      return false
    elsif !entity.is_a?(Sketchup::ComponentInstance) && !entity.is_a?(Sketchup::Group)
      return false
    else
      for key in get_attribute_list(entity)
        formula = get_attribute_formula(entity, key)
        if formula != nil
          if key == "x" || key == "y" || key == "z" ||
             key == "rotx" || key == "roty" || key == "rotz"
            set_attribute(entity, "_has_movetool_behaviors", 1.0)
            return true
          elsif formula.index(/(\b)(x|y|z|rotx|roty|rotz)(\b)/i) != nil
            set_attribute(entity, "_has_movetool_behaviors", 1.0)
            return true
          end
        end
      end

      if entity.is_a?(Sketchup::ComponentInstance)
        subentities = entity.definition.entities
      else
        subentities = entity.entities
      end
      dc_name = get_entity_formula_name(entity)
      for subentity in subentities
        if subentity.is_a?(Sketchup::ComponentInstance) ||
           subentity.is_a?(Sketchup::Group)
          for key in get_attribute_list(subentity)
            formula = get_attribute_formula(subentity, key)
            if formula != nil
              reg_exp = Regexp.new('(\b)(' + dc_name + '|parent)!(x|y|z|rotx|roty|rotz)(\b)', true)
              if formula.index(reg_exp) != nil
                set_attribute(entity, "_has_movetool_behaviors", 1.0)
                return true
              end
            end
          end
        end
      end

      set_attribute(entity, "_has_movetool_behaviors", 0.0)
      return false
    end
  end

  # Returns true or false based on whether an entity's children have "behaviors"
  # that could require a redraw of their geometry, position, or appearance.
  #
  # This method relies on the method has_behaviors to do its check.
  #
  #   Args:
  #     entity: reference to the entity we want to check for behaviors
  #
  #   Returns:
  #     boolean: true if behaviors are present on any of the children
  def children_have_behaviors(entity)
    if entity.is_a?(Sketchup::ComponentInstance)
      subentities = entity.definition.entities
    elsif entity.is_a?(Sketchup::Group)
      subentities = entity.entities
    else
      return false
    end
    for subentity in subentities
      if subentity.is_a?(Sketchup::ComponentInstance) ||
         subentity.is_a?(Sketchup::Group)
        if has_behaviors(subentity)
          return true
        end
      end
    end
    return false
  end

  # Determines whether an entity has "instance cached" attributes on it, meaning
  # there are attributes in the dynamic_components dictionary that have a key
  # prefix like '_inst_', as defined in @instance_cache_prefix. If it finds any
  # of these, it deletes them and applies them back to the instance. This is the
  # mechanism for sending current "instance" settings along for the ride when
  # a user saves out a definition.
  #
  #   Args:
  #     entity: reference to the entity we want to check for behaviors
  #
  #   Returns:
  #     boolean: true if we found some cached values to apply
  def apply_instance_cache(entity)
    if entity == nil
      return false
    elsif entity == false
      return false
    elsif !entity.is_a?(Sketchup::ComponentInstance)
      return false
    else
      found_cache = false
      for name in get_instance_cache_list(entity)
        found_cache = true
        value = entity.definition.get_attribute @dictionary_name, name
        entity.definition.delete_attribute @dictionary_name, name
        new_name = name[@instance_cache_prefix.length..999]
        entity.definition.set_attribute @dictionary_name, new_name, value
      end
      return found_cache
    end
  end

  # Determines whether an entity has "instance cached" attributes on it.
  #
  #   Args:
  #     entity: reference to the entity we want to check for behaviors
  #
  #   Returns:
  #     boolean: true if we found some cached values to apply
  def has_instance_cache(entity)
    if entity == nil
      return false
    elsif entity == false
      return false
    elsif !entity.is_a?(Sketchup::ComponentInstance)
      return false
    else
      found_cache = false
      for name in get_instance_cache_list(entity)
        new_name = name[@instance_cache_prefix.length..999]
        # There are some older components that will contain instance
        # cached attributes that are NOT core attributes that require
        # a redraw. By checking the cached attribute against our list
        # of core, instance-level attributes, we can determine if
        # this component "has a cache", meaning it needs to be redrawn
        # on placement in the model.
        if @instance_attributes[new_name.downcase] == true
          found_cache = true
        end
      end
      return found_cache
    end
  end

  # Takes all DC attributes attached to an instance and stores them in
  # definition-level attributes with a "_inst_" prefix.
  #
  #   Args:
  #     entity: reference to the instance we want to cache.
  #
  #   Returns:
  #     nothing
  def store_instance_cache(entity)
    if entity.is_a?(Sketchup::ComponentInstance)
      Sketchup.active_model.start_operation translate("Save As"),
        true, false, true
      for name in get_instance_attribute_list(entity)
        # Just because there is an attribute stored on the instance does
        # not mean that we need to store it on the instance cache. Only
        # core, instance-level attributes need to be preserved. The list of
        # these attributes are stored in @instance_attributes, so we can
        # compare the found attribute with our list to determine whether
        # to store it.
        if @instance_attributes[name.downcase] == true
          value = entity.get_attribute @dictionary_name, name
          new_name = @instance_cache_prefix + name
          entity.definition.set_attribute @dictionary_name, new_name, value
        end
      end
      Sketchup.active_model.commit_operation
    end
  end

  # Takes a single DC attribute attached to an instance and stores it in
  # definition-level attributes with a "_inst_" prefix.
  #
  #   Args:
  #     entity: reference to the instance we want to cache.
  #     name: attribute name to store
  #
  #   Returns:
  #     nothing
  def store_instance_cache_attribute(entity, name)
    value = get_attribute_value(entity, name)
    formula = get_attribute_formula(entity, name)
    label = get_attribute_label(entity, name)
    units = get_attribute_units(entity, name)
    access = get_attribute_access(entity, name)
    options = get_attribute_options(entity, name)
    formlabel = get_attribute_formlabel(entity, name)
    formulaunits = get_attribute_formulaunits(entity, name)

    pre = @instance_cache_prefix
    defn = entity.definition
    dict = @dictionary_name
    defn.set_attribute dict, pre + name, value
    defn.set_attribute dict, pre + "_" + name + "_formula", formula
    defn.set_attribute dict, pre + "_" + name + "_label", label
    defn.set_attribute dict, pre + "_" + name + "_units", units
    defn.set_attribute dict, pre + "_" + name + "_access", access
    defn.set_attribute dict, pre + "_" + name + "_options", options
    defn.set_attribute dict, pre + "_" + name + "_formlabel", formlabel
    defn.set_attribute dict, pre + "_" + name + "_formulaunits", formulaunits
  end

  # Erases all DC attributes attached to a definition with a "_inst_" prefix.
  #
  #   Args:
  #     entity: reference to the instance we want to cache.
  #     start_operation: optional boolean, if true create a transaction
  #   Returns:
  #     nothing
  def clear_instance_cache(entity, start_operation = true)
    if start_operation == true
      Sketchup.active_model.start_operation translate("Save As"),
        true, false, true
    end
    if entity.is_a?(Sketchup::ComponentInstance)
      for name in get_instance_cache_list(entity)
        entity.definition.delete_attribute @dictionary_name, name
      end
    end
    if start_operation == true
      Sketchup.active_model.commit_operation
    end
  end

  # Takes two parameters. If the first parameter "has a value," meaning it's
  # not null or false, then the method return the first parameter. Otherwise
  # it return the 2nd parameter.
  #
  #   Args:
  #     val1: first parameter to check
  #     val2: second parameter to return if the first is "empty"
  #
  #   Returns:
  #     object reference: either val1 or val2
  def second_if_empty(val1, val2)
    if val1 == nil
      return val2
    elsif val1 == false
      return val2
    else
      return val1
    end
  end

  # Takes two parameters. If the first parameter is "NaN", then the method
  # returns the second parameter. Otherwise it returns the 1st parameter.
  #
  #   Args:
  #     val1: first parameter to check
  #     val2: second parameter to return if the first is "NaN"
  #
  #   Returns:
  #     object reference: either val1 or val2
  def second_if_nan(val1, val2)
    if val1.to_s == "NaN"
      return val2
    else
      return val1
    end
  end

  # Loops across all attributes attached to a given entity and "runs" their
  # formulas. The result of each formula evaluation is then stored directly
  # into the value of the attribute.
  #
  #   Args:
  #     entity: reference to the entity we want to run formulas for
  #
  #   Returns:
  #     Nothing
  def run_all_formulas(entity)
    for attribute_name in get_attribute_list(entity)
      run_formula(entity, attribute_name)
    end
  end

  # Loops across all attributes attached to a given entity and "runs" their
  # formulas if that formula contains a call to the facearea function. This
  # is needed because that functions must be fired after all
  # of the faces have been rescaled.
  #
  #   Args:
  #     entity: reference to the entity we want to run formulas for
  #
  #   Returns:
  #     Nothing
  def run_facearea_formulas(entity)
    for name in get_attribute_list(entity)
      formula = get_attribute_formula(entity, name)
      if formula.to_s.downcase.include?("facearea") != nil
        # Clear out the @formula_results from before or we'll just get
        # the same cached result.
        @formula_results[entity.entityID.to_s + "_" + formula.to_s] = nil
        result, error = get_formula_result(entity, name, true)
        if result != nil
          set_attribute(entity, name, result)
        end
      end
    end
  end

  # Takes an entity and an attribute name and "runs" any formula defined for
  # that attribute. The result of the formula evaluation is then stored directly
  # into the value of the attribute.
  #
  #   Args:
  #     entity: reference to the entity
  #     name: name of the attribute we want to run the formulas for
  #
  #   Returns:
  #     Nothing
  def run_formula(entity, name)
    result, error = get_formula_result(entity, name, true)
    if result != nil
      set_attribute(entity, name, result)
    end
  end

  # Walks across all entities in a SketchUp model and creates a simple CSV
  # report containing all attributes that are defined. This is a recursive
  # method that will report on all sub-entities.
  #
  #   Args:
  #     entity_list: array of entities to run the report for
  #     parent_name: string containing the parent name of the list
  #
  #   Returns:
  #     array: a string with the report and filetype on success, false on
  #            failure.
  def generate_report_string(entity_list, parent_name = "",
                                          filename = "report.html", level = 1)

    # Bail out if the entities list is empty.
    if entity_list == nil
      return false
    elsif entity_list.length == 0
      return false
    end

    # if this is the top level of our recursion, then clean out attribute list
    if parent_name == ""
      Sketchup.active_model.start_operation translate("Generate Model Report"),
        true
      DCProgressBar::clear()
      @report_attribute_list = []
      @title_array = []
      @title_array.push(translate("PATH"))
      @title_array.push(translate("LEVEL"))
      @title_array.push(translate("ENTITY NAME"))
      @title_array.push(translate("ENTITY DESCRIPTION"))
      @title_array.push(translate("DEFINITION NAME"))
      @title_array.push(translate("LAYER"))
      @volume_title = translate("ENTITY VOLUME")
      @title_array.push(@volume_title)

      # Create some global structures to store our report data in as
      # it is built.
      @report_data = []
      @totals_by_att_name = {}
      @totals_by_att_name[@volume_title] = 0.0
    end

    # Calculate the file extension, including the period.
    dot_position = second_if_empty(filename.rindex("."), -1)
    filetype = (filename[dot_position..-1]).downcase

    if filetype == ".csv"
      doc_start = "${TITLEROW}"
      doc_end = ""
      row_start = ""
      row_end = "\n"
      cell_start = ""
      cell_mid = ","
      cell_end = ","
    else # default to html
      doc_start = "<html><head><meta http-equiv=\"Content-Type\" " +
                  "content=\"text/html; charset=utf-8\"></head>\n<style>" +
                  "table {\n" +
                  "  padding: 0px;\n" +
                  "  margin: 0px;\n" +
                  "  empty-cells: show;\n" +
                  "  border-right: 1px solid silver;\n" +
                  "  border-bottom: 1px solid silver;\n" +
                  "  border-collapse: collapse;\n" +
                  "}\n" +
                  "td {\n" +
                  "  padding: 4px;\n" +
                  "  margin: 0px;\n" +
                  "  border-left: 1px solid silver;\n" +
                  "  border-top: 1px solid silver;\n" +
                  "  font-family: sans-serif;\n" +
                  "  font-size: 9pt;\n" +
                  "  vertical-align: top;\n" +
                  "}\n</style>\n" +
                  "<table border=1>${TITLEROW}"
      doc_end = "</table></html>"
      row_start = "   <tr>\n"
      row_end = "   </tr>\n"
      cell_start = "    <td>"
      cell_mid = "</td>\n    <td>"
      cell_end = "</td>\n"
    end

    # Loop across every entity and build up the report data.
    for entity in entity_list

      # Create an array of cell data.
      cell_data = []
      if entity.is_a?(Sketchup::Model) || entity.is_a?(Sketchup::Group) ||
         entity.is_a?(Sketchup::ComponentInstance)

        # Add to list of attributes if we find some that aren't on the list.
        for attribute_name in get_attribute_list(entity, false, true)
          if @report_attribute_list.include?(attribute_name) == false
            if attribute_name[0..0] != "_"
              @title_array.push(attribute_name.upcase)
              @report_attribute_list.push attribute_name
            end
          end
        end

        # Fetch classifier data.
        classifier_data = get_classifier_data(entity)
        classifier_data.each do |column, value|
          unless @report_attribute_list.include?(column)
            @report_attribute_list.push(column)
            @title_array.push(column)
          end
        end

        entity_name = entity.name
        if entity_name.length < 1
          if entity.is_a?(Sketchup::ComponentInstance)
            entity_name = entity.definition.name
          elsif entity.is_a?(Sketchup::Model)
            entity_name = translate("Model")
          else
            entity_name = translate("Unnamed Part")
          end
        end
        cell_data.push(parent_name + entity_name)
        cell_data.push(level)
        cell_data.push(entity.name)
        cell_data.push(entity.description)
        if entity.is_a?(Sketchup::ComponentInstance)
          cell_data.push(entity.definition.name)
        else
          cell_data.push("-")
        end
        cell_data.push(entity.layer.name)

        # Add a column for volume and keep a running total.
        vol = ""
        if entity.is_a?(Sketchup::ComponentInstance) || entity.is_a?(Sketchup::Group)
          if entity.manifold?
            vol = fix_float(entity.volume)
            @totals_by_att_name[@volume_title] =
              @totals_by_att_name[@volume_title] + vol
          end
        end
        cell_data.push(vol)

        # add to our report results
        for attribute_name in @report_attribute_list
          value = get_attribute_value(entity, attribute_name)
          if value.nil?
            value = classifier_data[attribute_name]
          else
            # If the value is anything but a string, convert down to a number.
            if get_attribute_formulaunits(entity, attribute_name, true) != "STRING"
              value = @conv.clean_number(value)
            end
          end
          if value.kind_of? Float
            if value.to_s.include?("e-")
              value = 0.0
            end
          end
          if value == nil
            value = ""
          end
          if value == "0.0"
            value = ""
          end
          # TODO(Thom): The handling of the types here seem fragile to me. But
          # I'm not touching big parts of the code to change that now. But
          # something to look into.
          if value.is_a?(TrueClass) || value.is_a?(FalseClass)
            value = value.to_s
          end

          cell_data.push(value)
        end

        @report_data.push(cell_data)

        if entity.is_a?(Sketchup::ComponentInstance)
          subentities = entity.definition.entities
        else
          subentities = entity.entities
        end
        entity_name_without_slashes = entity_name.gsub(/\//, "_")
        continue = generate_report(subentities, parent_name +
                                                entity_name_without_slashes + "/", filename, level + 1)
        if continue == false
          user_cancelled_report = true
          break
        end
      end
    end

    # If the user cancelled the report because it was taking too long, or
    # if the report is empty because no reportable entities were passed in,
    # then exit silently.
    if user_cancelled_report == true || @report_data.length == 0
      DCProgressBar::clear()
      clean_up_report_data()
      return false
    end

    # Otherwise, let's actually output the report if we're at the end.
    if parent_name == ""
      report_string = doc_start

      longest_row_length = @report_data.last.length

      totals_row = []
      for att_name in @title_array
        total = @conv.clean_number(@totals_by_att_name[att_name]).to_f
        if total == 0.0
          total = "-"
        end
        totals_row.push total
      end
      totals_row[0] = translate("TOTALS")
      @report_data.push totals_row

      for cell_data in @report_data
        report_string += row_start
        for i in 0..(longest_row_length - 1)
          value = cell_data[i]
          if value.kind_of? String
            if value.to_i.to_s == value.to_s || value.to_f.to_s == value.to_s
              typed_cell_start = cell_start.gsub(/\$\{DATATYPE\}/, "Number")
            else
              typed_cell_start = cell_start.gsub(/\$\{DATATYPE\}/, "String")
            end
          else
            typed_cell_start = cell_start.gsub(/\$\{DATATYPE\}/, "Number")
          end
          report_string += typed_cell_start
          if filetype == ".html"
            report_string += clean_for_xml(value)
          else
            # File type is CSV
            value = value.to_s
            value = value.gsub(/\"/, '""')
            value = '"' + value + '"'
            report_string += value
          end
          report_string += cell_end
        end
        report_string += row_end
      end

      report_string += doc_end
      typed_cell_start = cell_start.gsub(/\$\{DATATYPE\}/, "String")
      report_string.gsub!(/\$\{TITLEROW\}/, row_start + typed_cell_start +
                                            @title_array.join(cell_mid) + cell_end + row_end)

      return [report_string, filetype]
    else
      return DCProgressBar.advance()
    end
  end

  # Walks across all entities in a SketchUp model and creates a simple CSV
  # report containing all attributes that are defined.
  #
  # Once the report is generated, a dialog is shown to allow the user to save
  # their report to disk, and gives them the option to immediately open it using
  # their default .CSV tool, such as excel.
  #
  #   Args:
  #     entity_list: array of entities to run the report for
  #     parent_name: string containing the parent name of the list
  #
  #   Returns:
  #     boolean: false if the user cancelled the report
  def generate_report(entity_list, parent_name = "", filename = "report.html", level = 1)
    result = generate_report_string(entity_list, parent_name, filename, level)
    unless result.is_a?(Array)
      return result
    end
    report_string, filetype = result
    save_report_to_file(report_string, filetype)
    DCProgressBar::clear()
    Sketchup.active_model.commit_operation
    clean_up_report_data()
    return true
  end

  # Saves a string to a local file, prompting the user with a save dialog.
  #
  #  Args:
  #    report_string: The string to save to a file.
  #    filetype: The file extension to save.
  def save_report_to_file(report_string, filetype)

    # Open a save dialog on the last known path, (passing nil as the save path
    # does that automatically.)
    default_save_name = Time.new.strftime("%Y-%m-%d") + filetype
    path = UI.savepanel translate("Save Report"), nil, default_save_name
    if path
      begin
        # If the user enters a weird file name such as "myFile.csx", then
        # append the filetype so it becomes "myFile.csx.csv"
        current_extension = path.downcase[(path.length - filetype.length)..999]
        if current_extension != filetype
          path = path + filetype
        end
        file = File.new(path, "w")
        file.print report_string
        file.close

        # The mac can't open documents with openURL, so we have a slightly
        # different interaction between the two platforms.
        if @is_mac
          msg = translate("Your report was saved successfully.")
          action = UI.messagebox(msg)
        else
          msg = translate("Your report was saved successfully.") + "\n" +
                translate("Would you like to open it now?")
          action = UI.messagebox(msg, MB_YESNO)
          if action == 6 # Yes
            UI.openURL(path)
          end
        end
      rescue
        msg = translate("There was an error saving your report.") + "\n" +
              translate("Please make sure it is not open in any other software " +
                        "and try again.")
        UI.messagebox(msg)
      end
    end
  end

  # Fetches classifier data for the entity and returns a hash.
  #   Args:
  #     entity: the entity to fetch classifier data for.
  #   Returns:
  #     A dictionary of classifier data.
  #
  def get_classifier_data(entity)
    classifier_data = {}
    Classifier.report_classifications(entity) { |column, value|

      # The classification column is a special case where we need to list
      # all the classification types used.
      if column == Classifier::CLASSIFICATION_TYPE
        existing_data = classifier_data[column]
        if existing_data
          value = "#{existing_data}, #{value}"
        end
      end
      classifier_data[column] = value
    }
    return classifier_data
  end

  # Cleans out the global variables that are used by generate_report
  # Args:
  #   none
  # Returns:
  #   nothing
  def clean_up_report_data()
    @report_attribute_list = nil
    @title_array = nil
    @report_data = nil
    @totals_by_att_name = nil
  end

  ##############################################################################
  # WebDialog Callbacks
  ##############################################################################
  private

  # General callback handler for all JS-to-Ruby invocations. This method
  # should be invoked as the last step in processing a JavaScript request.
  # Standardized methods on the JavaScript side of the bridge should be made
  # available to this routine by including sketchup.js
  #
  # Args:
  #   dialog: reference to the WebDialog that made the original request.
  #   funcname: the invoking function name, passed back to the JavaScript.
  #   query:  the query string, the portion of a skp: URL after the @ sign.
  #   response: the data that should be returned to the caller.
  #   fault: a fault code, if any, when an error has occurred.
  def js_callback(dialog, funcname, query, response, fault)
    # convert query string (a=b&c=d) into a hash of key value pairs
    hash = query_to_hash(dialog, query)
    queryid = hash["queryid"]

    # set/clear the fault data via the JS-side fault handler
    str = fault.nil? ? "" : fault.gsub('"', '\"')
    if (fault != nil)
      script = 'su.setRubyFault_("' + queryid + '","' + str + '");'
      dialog.execute_script(script)
    end

    # set/clear the response data via the JS-side response handler
    str = response.nil? ? "" : response.gsub('"', '\"')
    if (response != nil)
      script = 'su.setRubyResponse_("' + queryid + '","' + str + '");'
      dialog.execute_script(script)
    end

    # process whatever callbacks we might have been passed explicitly
    begin
      if (fault != nil)
        fname = hash["onfailure"] || hash["onFailure"]
        if (fname != nil)
          script = 'su.rubyCallback_("' + fname + '","' + queryid + '");'
          dialog.execute_script(script)
        end
      else
        fname = hash["onsuccess"] || hash["onSuccess"]
        if (fname != nil)
          script = 'su.rubyCallback_("' + fname + '","' + queryid + '");'
          dialog.execute_script(script)
        end
      end
    ensure
      fname = hash["oncomplete"] || hash["onComplete"]
      if (fname != nil)
        script = 'su.rubyCallback_("' + fname + '","' + queryid + '");'
        dialog.execute_script(script)
      end
    end

    script = 'su.clearRubyData_("' + queryid + '");'
    dialog.execute_script(script)
  end

  # General logging method allowing the JavaScript interface to send
  # messages to the Ruby console.
  #
  # Args:
  #   params:  a string containing the message content to be output in the
  #   message key, and optional separator, timestamp, and prefix keys
  #   defining a separator to output prior to outputting the log message as
  #   well as a timestamp and message prefix such as 'JS:'.
  def js_log(dialog, params)

    # treat empty parameter as a request to output a blank line, just like a
    # call to puts without a parameter would do
    if (params == nil)
      puts
      return
    end

    # all other parameters should have come in key/value form so convert
    hash = query_to_hash(dialog, params)

    # optional separator can be output if provided. NOTE that even an empty
    # string can be used here to create a new (blank) line in the log
    if (hash["separator"] != nil)
      puts(hash["separator"])
    end

    # build up message text. should be timestamp prefix: message
    str = ""
    str += hash["timestamp"].to_s + " " unless hash["timestamp"] == nil
    str += hash["prefix"].to_s + " " unless hash["prefix"] == nil
    str += hash["message"].to_s unless hash["message"] == nil
    puts(str)
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It pulls a translated string from the users strings file. It
  # then sends the data up to the WebDialog as a javascript var called
  # dataFromRuby.
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     string_to_translate: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def pull_translation(dialog, string_to_translate)
    dialog.execute_script('var dataFromRuby = "' + translate(string_to_translate) + '"')
  end

  # WebDialog callback method. This method is attached as a valid callback
  # for any WebDialog created by the DynamicComponents class.
  #
  # Passes the entire dictionary of localizable strings to the WebDialog
  # using the Ruby/JS bridge
  #
  def pull_translations(dialog, params)
    json = "{"
    $dc_strings.GetStrings().keys.each do |string|
      json += "'" + string + "': '" +
              $dc_strings.GetString(string).gsub("'", "\\'") + "',"
    end
    json += "'':''}"
    #puts(json)
    js_callback(dialog, "pull_translations", params, json, nil)
  end

  # WebDialog callback method. This method is attached as a valid callback
  # for any WebDialog created by the DynamicComponents class.
  #
  # Tells SU to place a component. Once placed, it will attach an passed DC
  # attributes and fire a redraw.
  #
  def do_place_component(dialog, data)
    Sketchup.active_model.start_operation translate("Place Component"), true
    params = query_to_hash(dialog, data)

    # Confirm that we've been passed a definition id to place.
    definition_id = params["definition_id"]
    if definition_id == nil
      puts "ERROR: no definition_id passed."
      return
    end

    # Loop across our definitions to find the one that they want to place.
    for entity in Sketchup.active_model.definitions
      if entity.entityID.to_s == definition_id.to_s
        definition = entity
      end
    end

    # Confirm that we found a valid definition.
    if definition == nil
      puts "ERROR: Could not find definition #" + definition_id.to_s
      return
    end

    # Check our params set for any attributes to attach.
    has_attributes = false
    for key in params.keys
      if key =~ /__value/
        has_attributes = true
        break
      end
    end

    # If there are attributes to attach, then do that now.
    if has_attributes == true

      # Add the definition to the model.
      instance = Sketchup.active_model.entities.add_instance definition,
                                                             Geom::Point3d.new(0.0, 0.0, 0.0)

      # Make sure it's unique and not a 3D warehouse shell.
      instance = explode_if_3d_warehouse(instance)
      make_unique_if_needed(instance)
      definition = instance.definition

      # Apply our attributes to the thing by leveraging the same instance cache
      # mechanism used to handle right click > Save as.
      for key in params.keys
        if key =~ /__value/
          name = key.split("__").first
          value = params[key]
          if name != nil
            if value.to_i.to_s == value.to_s || value.to_f.to_s == value.to_s
              value = value.to_f
            end
            set_attribute(definition, name, value, nil, nil, nil, nil,
                          nil, nil, nil, nil, @instance_cache_prefix)
            set_forced_config_value(instance, name, value)
          end
        end
      end
      apply_instance_cache(instance)

      # Now that our attributes are applied, redraw the component, accounting
      # for the placed size, etc.
      lenx, leny, lenz = instance.scaled_size
      instance.set_last_size(lenx, leny, lenz)
      definition_count = Sketchup.active_model.definitions.count
      determine_movetool_behaviors(instance)
      DCProgressBar::clear()
      redraw(instance)
      DCProgressBar::clear()

      # If in the course of the redraw, a new definition was created, then we
      # can detect that here and place the new definition.
      if Sketchup.active_model.definitions.count > definition_count
        definition = Sketchup.active_model.definitions[definition_count - 1]
      end

      # Store the current material so we can apply it once the user places their
      # component.
      @forced_material[definition.to_s] = instance.material
    end

    Sketchup.active_model.commit_operation
    if instance != nil
      instance.erase!
    end

    # Place the definition.
    Sketchup.active_model.place_component definition

    json = "{}"
    js_callback(dialog, "do_place_component", data, json, nil)
  end

  # WebDialog callback method. This method is attached as a valid callback
  # for any WebDialog created by the DynamicComponents class.
  #
  # Tells SU to place a component. Once placed, it will attach an passed DC
  # attributes and fire a redraw.
  #
  def do_load_from_url(dialog, data)
    params = query_to_hash(dialog, data)
    url = params["url"].to_s
    downloader = DCDownloader.new
    Sketchup.active_model.definitions.load_from_url url, downloader

    if downloader.error == nil
      last_def = Sketchup.active_model.definitions.count - 1
      definition = Sketchup.active_model.definitions[last_def]
      json = "{definitionID:'" + clean_for_json(definition.entityID) + "'}"
    else
      json = "{error:'" + clean_for_json(downloader.error) + "'}"
    end
    js_callback(dialog, "do_load_from_url", data, json, nil)
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It takes a number representing a length in inches and returns a formatted
  # string representing that length in the user's default units (as defined in
  # their SketchUp template.)
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     length: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def pull_format_length(dialog, length)
    result = Sketchup.format_length(length.to_f)
    result = result.gsub(/"/, "&quot;")
    dialog.execute_script('var dataFromRuby = "' + result + '"')
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It closes the WebDialog that called it and redraws any entity whose ID
  # is passed as part of the data string. It also clears out the
  # @entity_pointers
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def do_close(dialog, data)
    params = query_to_hash(dialog, data)
    if params["id"]
      entity = @entity_pointers[params["id"]]
      dialog.close()
      if has_behaviors(entity)
        Sketchup.active_model.start_operation translate("Redraw"), true
        lenx, leny, lenz = entity.scaled_size
        entity.set_last_size(lenx, leny, lenz)
        determine_movetool_behaviors(entity)
        DCProgressBar::clear()
        redraw(entity)
        DCProgressBar::clear()
        Sketchup.active_model.commit_operation
        refresh_configure_dialog()
      end
    else
      dialog.close()
    end
    # TODO(scottlininger): Since dialogs aren't necessarily modal anymore, it's
    # possible that the config window's do_close call could wipe out some
    # entity pointers that a still-open manager window could need. Need to make
    # this smarter so that doesn't happen. Perhaps attach an @entity_pointers
    # array to each individual WebDialog.
    #@last_selected_entity = nil
    #@entity_pointers = {}
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It toggles the simplification of the Classifier attributes. Toggling this
  # trigger a rebuild of the webdialog content - any unsaved changes will be
  # lost.
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def do_set_simplify(dialog, data)
    params = query_to_hash(dialog, data)
    @simplify = (params["checked"].downcase == "true")
    refresh_configure_dialog()
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It redraws the entity that is passed in the id param
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def do_redraw(dialog, data)
    params = query_to_hash(dialog, data)
    if params["id"]
      entity = @entity_pointers[params["id"]]
      if has_behaviors(entity)
        Sketchup.active_model.start_operation translate("Redraw"), true
        lenx, leny, lenz = entity.scaled_size
        entity.set_last_size(lenx, leny, lenz)
        determine_movetool_behaviors(entity)
        DCProgressBar::clear()
        redraw(entity)
        DCProgressBar::clear()
        Sketchup.active_model.commit_operation
        refresh_configure_dialog()
      end
    end
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It deletes the attribute identified by an entity's ID and an attribute
  # name, as passed in the data string.
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def do_delete_attribute(dialog, data)
    Sketchup.active_model.start_operation translate("Delete Attribute"), true
    params = query_to_hash(dialog, data)
    entity = @entity_pointers[params["id"]]
    name = params["name"].downcase
    delete_attribute(entity, name)
    determine_movetool_behaviors(entity)

    if has_behaviors(entity, true)
      DCProgressBar::clear()
      redraw(entity)
      DCProgressBar::clear()
    end

    # Refresh the component browser in case our behaviors flag has changed.
    UI.refresh_inspectors

    refresh_configure_dialog()
    Sketchup.active_model.commit_operation
    js_callback(dialog, "delete_attribute", data, "true", nil)
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It creates or changes the attribute identified by an entity's ID and an
  # attribute name, as passed in the data string. In addition to a value, the
  # attribute's formula, label, access, etc. can all be defined here.
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  # amtf 修改属性值时调用👇
  def push_attribute(dialog, data)
    params = query_to_hash(dialog, data)
    entity = @entity_pointers[params["id"]]
    puts "push_attribute params 👉 #{params}"
    if entity == nil
      return
    end
    if !entity.valid?
      return
    end

    if params["name"] == "_iscollapsed"
      Sketchup.active_model.start_operation translate("Collapse"), true
    elsif params["name"] == "_name"
      Sketchup.active_model.start_operation translate("Rename"), true
    elsif params["name"] == "_lengthunits"
      Sketchup.active_model.start_operation translate("Set Units"), true
    elsif get_cascading_attribute(entity, params["name"]) == nil &&
          params["value"] == nil && params["access"] == nil
      Sketchup.active_model.start_operation translate("Add Attribute"), true
    else
      Sketchup.active_model.start_operation translate("Change Attribute"), true
    end

    # If no lengthunits are set on our entity, then set them now.
    if params["name"] != "_lengthunits" &&
       get_entity_lengthunits(entity, false) == nil
      default_length_units = get_entity_lengthunits(entity, true)
      set_attribute(entity, "_lengthunits", default_length_units)
    end

    # If the attribute we're changing is _name, then we need to replace any
    # formula references to this entity. So if a formula contains oldName!X,
    # we must change it to newName!X.
    if params["name"] == "_name"
      # Clean out any characters that are formula operators from the name.
      params["value"] = params["value"].gsub(/[\*\/\+\-\=\>\<\\(\)\"]/, "")

      parent = entity.parent
      old_name = get_entity_formula_name(entity)
      new_name = params["value"]
      if parent.to_s.index("Sketchup::Model") == nil
        parent = parent.instances[0]
        replace_formula_references(parent, old_name, new_name)
      else
        replace_formula_references(entity, old_name, new_name)
      end

      # We need to rename any copies first.
      for sibling in entity.parent.entities
        if sibling.typename == entity.typename
          if sibling.name.index(old_name + " copy") == 0
            sibling_copy_number = copy_number_of(sibling)
            sibling.name = name_of_copy(params["value"], sibling_copy_number)
          end
        end
      end
    elsif get_attribute_value(entity, "_name") == nil
      # If this entity does not yet have an _name attribute, then set that
      # now so future formula references will not break.
      this_name = get_entity_formula_name(entity)
      set_attribute(entity, "_name", this_name)
    end

    # If the attribute we're changing is onclick, then we should wipe out any
    # animation state information that we'd previously recorded.
    if params["name"] == "onclick"
      delete_onclick_state(entity)
    end

    if params["value"] != nil
      params["value"] = apply_relative_move(entity, params["name"],
                                            params["value"])
    end

    # Stored our value in this @forced_config_values hash. This hash takes
    # precedence over any value or formula for the current redraw cycle. This
    # hash will be emptied at the end of the redraw() method.
    clear_forced_config_values()
    if params["formula"] == nil
      set_forced_config_value(entity, params["name"], params["value"])
    end

    error = nil
    commit_after_redraw = true
    if params["formula"]
      # Do nothing.
    else
      params["formula"] = "ERASEFORMULA"
    end

    # Only delete formlabel when the caller explicitly allows it.
    can_delete_formlabel = params["canDeleteFormlabel"]
    if can_delete_formlabel.nil?
      can_delete_formlabel = false
    end

    make_unique_if_needed(entity)
    set_attribute(entity, params["name"], params["value"], params["formula"],
                  params["label"], params["access"], params["options"], params["formlabel"],
                  params["units"], error, params["formulaunits"], "", can_delete_formlabel)

    if (has_behaviors(entity, true) && params["name"] != "_iscollapsed" &&
        params["no_redraw"] == nil) || params["name"] == "scaletool" ||
       params["name"] == "x" || params["name"] == "y" ||
       params["name"] == "z" || params["name"] == "rotx" ||
       params["name"] == "roty" || params["name"] == "rotz" ||
       params["name"] == "lenx" || params["name"] == "leny" ||
       params["name"] == "lenz" || params["name"] == "hidden" ||
       params["name"] == "material"

      # We will always store which format version this component was edited with,
      # as well as a timestamp for when it was last modified.
      if entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::Model) ||
         entity.is_a?(Sketchup::ComponentDefinition)
        metadata_entity = entity
      else
        metadata_entity = entity.definition
      end
      metadata_entity.set_attribute @dictionary_name,
        "_formatversion", @format_version
      metadata_entity.set_attribute @dictionary_name,
        "_lastmodified", Time.new.strftime("%Y-%m-%d %H:%M")

      if params["formula"]
        determine_movetool_behaviors(entity)
      end
      if params["redraw_id"] != nil
        entity = @entity_pointers[params["redraw_id"]]
      end

      lenx, leny, lenz = entity.scaled_size
      entity.set_last_size(lenx, leny, lenz)
      DCProgressBar::clear()
      commit_after_redraw = redraw(entity)

      # If our last used tool was the scale tool then reselect the tool itself
      # to fire a redraw of the scale handles. This fixes a bug where the
      # handles can get out of sync on screen.
      if @dcobservers.last_tool_name == "ScaleTool"
        Sketchup.send_action "selectSelectionTool:"
        Sketchup.send_action "selectScaleTool:"
      end

      DCProgressBar::clear()
    end

    # Clear out our forced_config_values so we don't accidentally apply it
    # to the next redraw.
    clear_forced_config_values()

    # Refresh the component browser in case our behaviors flag has changed.
    UI.refresh_inspectors

    # We only need to refresh the configure dialog here, since push_attribute
    # calls only come from the manager panel, which refreshes itself once
    # it pushes down an attribute.
    refresh_configure_dialog()

    # Since the progress bar may have aborted the operation during the redraw,
    # only commit the operation if it still exists. Otherwise we can see
    # javascript errors on the PC under rare circumstances.
    if commit_after_redraw == true

      # Commit our operation.
      Sketchup.active_model.commit_operation

      js_callback(dialog, "push_attribute", data, "true", nil)
    end
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It renames a given attribute that is attached to the entity defined by the
  # passed id.
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def push_rename(dialog, data)
    params = query_to_hash(dialog, data)

    Sketchup.active_model.start_operation translate("Rename Attribute"), true

    entity = @entity_pointers[params["id"]]
    dc_name = get_attribute_value(entity, "_name")

    # Starting with the entity's parent if possible, recursively change
    # all mentions of the old attribute to the new one.
    # TODO(scottlininger): This will eventually need to be smart enough to look
    # for references to "parent" if the parent is the model.
    if entity.parent.to_s.include? "ComponentDefinition"
      # TODO(scottlininger): The instances[0] trick below could fail if there
      # are more than one instance out there and you only want to rename an
      # attribute in the 2nd one. However, this seems like a very strange case
      # that isn't worth worrying about.
      rename_attribute(entity.parent.instances[0], params["name"],
                       params["new_name"], params["new_label"], true, dc_name, true)
    end
    rename_attribute(entity, params["name"], params["new_name"],
                     params["new_label"], true, dc_name)

    Sketchup.active_model.commit_operation

    js_callback(dialog, "push_rename", data, "true", nil)
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It sets the SketchUp selection to contain an entity defined in the ID string
  # passed as part of the params data.
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def push_selection(dialog, data)

    # TODO(scottlininger): I've just identified a class of undo bugs caused
    # by this highlighting trick, and Tyler and I agree that the immediate
    # position should be to pull this feature back, then we'll look at putting
    # it back in down the road. Thus, we return.
    return

    params = query_to_hash(dialog, data)
    entity = @entity_pointers[params["id"]]
    if Sketchup.version.to_f < 7.0
      return
    end

    if entity == nil
      destroy_highlight_instance()
      return
    end

    if entity.valid? == false
      destroy_highlight_instance()
      return
    end

    # Load our definitions if we need to. First possibility is that the
    # definition exists but has been deleted.
    if @highlight_definition != nil
      if @highlight_definition.valid? == false
        @highlight_definition = nil
      end
    end

    # Now we can load the definition if we need it.
    if @highlight_definition.to_s == ""
      # highlight_path = Sketchup.find_support_file("(highlight).skp",
      #                                             $SKPS目录)
      highlight_path = File.join($SKPS目录, "(highlight).skp")

      definitions = Sketchup.active_model.definitions
      @highlight_definition = definitions.load highlight_path
      @highlight_definition.set_attribute @dictionary_name,
        "_hideinbrowser", true

      # highlight_path = Sketchup.find_support_file("(highlightsoft).skp",
      #                                             $SKPS目录)
      highlight_path = File.join($SKPS目录, "(highlightsoft).skp")
      definitions = Sketchup.active_model.definitions
      @highlight_definition_soft = definitions.load highlight_path
      @highlight_definition_soft.set_attribute @dictionary_name,
        "_hideinbrowser", true

      UI.refresh_inspectors
    end

    # Use the normal highlight by default, but switch to the softer
    # one if we'll be drawing the highlight outside the currently active
    # edit mode.
    @definition_to_use = @highlight_definition

    # If we're in edit mode, draw the highlight into the root of the model.
    # If we're not in edit mode, draw the highlight into the entity's parent,
    # unless the parent is the model
    if Sketchup.active_model.active_path != nil
      entities = Sketchup.active_model.entities
      bb = entity.bounds
    elsif entity.parent == Sketchup.active_model
      if entity.is_a?(Sketchup::ComponentInstance)
        entities = entity.definition.entities
        bb = entity.definition.bounds
      else
        entities = entity.entities
        bb = entity.bounds
      end
      @definition_to_use = @highlight_definition_soft
    else
      entities = entity.parent.entities
      bb = entity.bounds
      @definition_to_use = @highlight_definition_soft
    end

    xs = bb.max.x - bb.min.x + 0.5
    ys = bb.max.y - bb.min.y + 0.5
    zs = bb.max.z - bb.min.z + 0.5
    corner = Geom::Point3d.new(bb.min.x - 0.25, bb.min.y - 0.25, bb.min.z - 0.25)
    scaling = Geom::Transformation.scaling Geom::Point3d.new(0, 0, 0), xs, ys, zs
    translation = Geom::Transformation.translation corner

    destroy_highlight_instance()

    @highlight = entities.add_instance @definition_to_use, scaling
    @highlight.transform! translation
    @highlight.name = "(highlight)"

    # Invalidate our view to force a redraw with our new selection in place.
    Sketchup.active_model.active_view.invalidate
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It finds an attribute identified by an Entity ID and an attribute name, and
  # determines the "live value" for that attribute. It them sends the result
  # up to the WebDialog as a javascript var called dataFromRuby.
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def pull_live_value(dialog, data)
    params = query_to_hash(dialog, data)
    entity = @entity_pointers[params["id"]]
    result = get_live_value(entity, params["name"])
    result = clean_for_json(result.to_s)
    dialog.execute_script('var dataFromRuby = "' + result.to_s + '"')
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It shows a URL in the user's default browser
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def do_open_url(dialog, data)
    # This method always expects a string in the form "url=http://...",
    # so chop off the first 4 characters to get at our URL. This
    # simplifies the escaping logic to allow for '=' characters
    # in the URL.
    url = data[4..9999]

    # If they're trying to open a javascript call, ignore.
    if url.downcase =~ /javascript\:/
      return
    end

    UI.openURL(url)
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It shows a URL in the user's default browser
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def do_show_dialog(dialog, data)
    params = query_to_hash(dialog, data)
    show_dialog(second_if_empty(params["name"], "Dialog"),
                second_if_empty(params["url"], "http://www.sketchup.com"),
                second_if_empty(params["dialog_w"], 300),
                second_if_empty(params["dialog_h"], 300),
                second_if_empty(params["dialog_x"], 300),
                second_if_empty(params["dialog_y"], 300),
                false)
  end

  # WebDialog callback method. This method is attached as a valid callback for
  # any WebDialog created by the DynamicComponents class.
  #
  # It shows a URL in the user's default browser
  #
  #   Args:
  #     dialog: reference to the WebDialog that made the call
  #     data: string of the WebDialog params
  #
  #   Returns:
  #     Nothing
  def do_send_action(dialog, data)
    params = query_to_hash(dialog, data)

    active_model_only = []
    active_model_only.push Sketchup.active_model

    action = params["action"].to_s
    if action == "generateModelXML:"
      generate_report(active_model_only, "",
                      translate("Model.html"))
    elsif action == "generateSelectionXML:"
      generate_report(Sketchup.active_model.selection, "",
                      translate("Selection.html"))
    elsif action == "generateModelCSV:"
      generate_report(active_model_only, "",
                      translate("Model.csv"))
    elsif action == "generateSelectionCSV:"
      generate_report(Sketchup.active_model.selection, "",
                      translate("Selection.csv"))
    else
      Sketchup.send_action action
    end
  end

  public

  # This method takes a number of optional params and sets an attribute
  # containing all of the passed data.
  #
  # All attributes that DynamicComponents interact with are stored in an
  # AttributeDictionary called @dictionary_name. There is a particular naming
  # convention that is used to store both the attribute and any meta-data about
  # that attribute that needs to be stored. The following shows an example
  # of the actual attribute names that would be used to store all of this:
  #
  # - EXAMPLE 1: attribute called "Weight", with a value of 45, and a formula of
  #   "Part1!Weight+45".
  #
  #     weight = '45'
  #     _weight_label = 'Weight'
  #     _weight_formula = 'Part1!Weight+45'
  #
  # - EXAMPLE 2: attribute called "MyWidth", with a value of 15 and 3 options to
  #   choose from:
  #
  #     mywidth = '15'
  #     _mywidth_label = 'MyWidth'
  #     _mywidth_access = 'LIST'
  #     _mywidth_access = 'Small=10&Medium=15&Really+Large=25'
  #
  #   Args:
  #     entity: required, reference to the entity to attach to
  #     name: require string, name of the attribute to create or change
  #     value: required value, the numeric or string value to attach
  #     formula: optional, the string formula
  #     label: optional, the string label that appears to the author
  #     access: optional, the string access setting, such as "TEXTBOX"
  #     options: optional, the string list of valid options
  #     formlabel: optional, the string label that appears to the end user
  #     units: optional, the string containing the unit type, such as "INCHES"
  #     error: optional, the string containing the last parser error
  #     formulaunits: optional, the string containing the unit type, such as
  #         "LENGTH"
  #     name_prefix: optional, a string that will be appended as a suffix to
  #         the name of this attribute. Currently used to mark up an attribute
  #         as being a '_inst_' attribute, so that instance attributes can be
  #         preserved on a definition for purposes of right click > saving.
  #     can_delete_formlabel: optional, a boolean that determines if the
  #         formlabel attribute should be deleted when formlabel is nil.
  #
  #   Returns:
  #     Nothing
  # amtf 修改属性的底层函数
  def set_attribute(entity, name, value, formula = nil, label = nil, access = nil,
                                         options = nil, formlabel = nil, units = nil, error = nil,
                                         formulaunits = nil, name_prefix = "",
                                         can_delete_formlabel = false)
    name = name.downcase

    # Determine which level to write our attribute meta data to. Certain
    # attributes have their metadata stored at the instance level to best match
    # Sketchup's component model. (For groups, there's no explicit definition
    # to write to, so we always write meta attributes directly to the entity.)
    if @instance_attributes[name] == true || entity.is_a?(Sketchup::Group) ||
       entity.is_a?(Sketchup::ComponentDefinition) || entity.is_a?(Sketchup::Model)
      metadata_entity = entity
    else
      metadata_entity = entity.definition
    end

    # If an instance or group's attribute has a "live value," meaning it's an
    # internally-driven attribute such as X,Y, or Z, then default empty values
    # to that live value. (The method get_attribute_value returns the live
    # value.)
    # if !entity.is_a?(Sketchup::ComponentDefinition)
    #   if get_live_value(entity, name) != nil
    #     value = second_if_empty(value, get_attribute_value(entity, name))
    #   end
    # end
    # 如果实体不是组件定义（即不是 ComponentDefinition），且该属性（name）存在 “实时值”（如 x、y、z 等由组件位置 / 状态动态生成的值），则当 value 为空时，自动用 “实时值” 填充 value：
    if !entity.is_a?(Sketchup::ComponentDefinition)
      # get_live_value 检查该属性是否有实时值（如位置、旋转等动态计算的值）
      if get_live_value(entity, name) != nil
        # 若 value 为空，用当前属性的实时值（通过 get_attribute_value 获取）作为默认值
        # puts "传递的尺寸value 👉 #{value}"
        # 自动获取尺寸 = get_attribute_value(entity, name)
        # puts "自动获取尺寸 value 👉 #{自动获取尺寸}"
        value = second_if_empty(value, get_attribute_value(entity, name))
        # puts "最终取值的尺寸value 👉 #{value}"
      end
    end

    # Before we write any attribute, let's first erase any old value(s) so we're
    # sure not to have conflicting values at the instance vs. definition level.
    # TODO(scottlininger): In the future, if we implement the ability for the
    # author to explicitly choose the instance vs. definition to apply to, then
    # this check will need to go away, since it will then be perfectly valid
    # to have attributes at both levels.
    # 动态组件的属性可存储在 “实例”（ComponentInstance）或 “定义”（ComponentDefinition）中，delete_cascading_attribute 会同时清理这两个层级的旧值，确保新 value 能正确生效。
    delete_cascading_attribute(entity, name)

    # Note that for best behavior, we store our values on both the instance
    # and the definition. The instance attributes takes precedence when read
    # via get_cascading_attribute, which gives us nice reporting behavior
    # for repeating parts. But the fact that we also store it on the
    # definition means that we record it in a stable way to handle cases
    # where users manually drag subcomponents in from the in-model view of
    # the component browser.
    # 请注意，为获得最佳效果，我们会将值同时存储在实例和定义中。
    # 通过 get_cascading_attribute 方法读取时，实例属性具有优先级 —— 这能为重复组件提供良好的显示反馈效果。
    # 而将值同时存储在定义中的设计，是为了以稳定的方式记录属性，
    # 以处理用户从组件浏览器的 “在模型中” 视图手动拖拽子组件的场景。
    # puts "最终取值的尺寸value 👉 #{value.to_mm}"
    # puts "name_prefix + name 👉 #{name_prefix + name}"
    # puts "@dictionary_name 👉 #{@dictionary_name}"
    metadata_entity.set_attribute @dictionary_name, name_prefix + name, value
    entity.set_attribute @dictionary_name, name_prefix + name, value

    # TODO(scottlininger)- if passed nil for a formula, need to set
    # formula to nil (this is a temporary kludge)
    if formula == "ERASEFORMULA"
      delete_cascading_attribute(entity, "_" + name + "_formula")
      delete_cascading_attribute(entity, "_" + name + "_error")
    elsif formula != nil
      delete_cascading_attribute(entity, "_" + name + "_formula")
      delete_cascading_attribute(entity, "_" + name + "_error")
      metadata_entity.set_attribute @dictionary_name, name_prefix +
                                                      "_" + name + "_formula", formula
    end

    if label != nil
      delete_cascading_attribute(entity, "_" + name + "_label")
      metadata_entity.set_attribute @dictionary_name, name_prefix +
                                                      "_" + name + "_label", label
    end
    if access != nil
      delete_cascading_attribute(entity, "_" + name + "_access")
      metadata_entity.set_attribute @dictionary_name, name_prefix +
                                                      "_" + name + "_access", access
    end
    if options != nil
      delete_cascading_attribute(entity, "_" + name + "_options")
      metadata_entity.set_attribute @dictionary_name, name_prefix +
                                                      "_" + name + "_options", options
    end

    if formlabel != nil
      delete_cascading_attribute(entity, "_" + name + "_formlabel")
      metadata_entity.set_attribute @dictionary_name, name_prefix +
                                                      "_" + name + "_formlabel", formlabel
    elsif can_delete_formlabel
      delete_cascading_attribute(entity, "_" + name + "_formlabel")
    end

    if units != nil
      delete_cascading_attribute(entity, "_" + name + "_units")
      metadata_entity.set_attribute @dictionary_name, name_prefix +
                                                      "_" + name + "_units", units
    end
    if error != nil
      delete_cascading_attribute(entity, "_" + name + "_error")
      metadata_entity.set_attribute @dictionary_name, name_prefix +
                                                      "_" + name + "_error", error
    end
    if formulaunits == "DEFAULT"
      delete_cascading_attribute(entity, "_" + name + "_formulaunits")
    elsif formulaunits != nil
      delete_cascading_attribute(entity, "_" + name + "_formulaunits")
      metadata_entity.set_attribute @dictionary_name, name_prefix +
                                                      "_" + name + "_formulaunits", formulaunits
    end

    # If the attribute that is being set is an instance-level attribute, then
    # we cache a copy of the value at the definition level. Thus, the definition
    # will always contain the *last* instance level attributes set. This allows
    # us to drag a component from the component browser and have it reflect
    # any instance-level formulas, etc. that have been set. (See
    # handle_place_component method for that part.)
    if @instance_attributes[name] == true &&
       entity.is_a?(Sketchup::ComponentInstance)
      store_instance_cache_attribute(entity, name)
    end
  end

  # This method sets the formula on a given attribute.
  #
  #   Args:
  #     entity: required, reference to the entity to attach to
  #     name: required string, name of the attribute to create or change
  #     formula: required string, the formula
  #
  #   Returns:
  #     Nothing
  def set_attribute_formula(entity, name, formula)
    name = name.downcase
    if @instance_attributes[name] == true || entity.is_a?(Sketchup::Group)
      metadata_entity = entity
    else
      metadata_entity = entity.definition
    end
    delete_cascading_attribute(entity, "_" + name + "_formula")
    delete_cascading_attribute(entity, "_" + name + "_error")
    metadata_entity.set_attribute @dictionary_name, "_" + name + "_formula", formula
  end

  # This method sets the formula error on a given attribute.
  #
  #   Args:
  #     entity: required, reference to the entity to attach to
  #     name: required string, name of the attribute to create or change
  #     formula: required string, the formula
  #
  #   Returns:
  #     Nothing
  def set_attribute_error(entity, name, error)
    name = name.downcase
    if @instance_attributes[name] == true || entity.is_a?(Sketchup::Group)
      metadata_entity = entity
    else
      metadata_entity = entity.definition
    end
    delete_cascading_attribute(entity, "_" + name + "_error")
    metadata_entity.set_attribute @dictionary_name, "_" + name + "_error", error
  end

  # This method deletes any attribute with a given name as well as all metadata
  # associated with that attribute, at both the instance and definition level.
  #
  #   Args:
  #     entity: required, reference to the entity to delete from
  #     name: require string, name of the attribute to delete
  #
  #   Returns:
  #     Nothing
  def delete_attribute(entity, name)
    if entity.is_a?(Sketchup::ComponentInstance)
      entity.definition.delete_attribute @dictionary_name, name
      entity.definition.delete_attribute @dictionary_name, "_" + name + "_formula"
      entity.definition.delete_attribute @dictionary_name, "_" + name + "_label"
      entity.definition.delete_attribute @dictionary_name, "_" + name + "_access"
      entity.definition.delete_attribute @dictionary_name, "_" + name + "_options"
      entity.definition.delete_attribute @dictionary_name, "_" + name + "_formlabel"
      entity.definition.delete_attribute @dictionary_name, "_" + name + "_units"
      entity.definition.delete_attribute @dictionary_name, "_" + name + "_error"
      entity.definition.delete_attribute @dictionary_name, "_" + name + "_formulaunits"

      # In addition to deleting the attribute, delete any instance-cached
      # attributes as well.
      pre = @instance_cache_prefix
      entity.delete_attribute @dictionary_name, pre + name
      entity.delete_attribute @dictionary_name, pre + "_" + name + "_formula"
      entity.delete_attribute @dictionary_name, pre + "_" + name + "_label"
      entity.delete_attribute @dictionary_name, pre + "_" + name + "_access"
      entity.delete_attribute @dictionary_name, pre + "_" + name + "_options"
      entity.delete_attribute @dictionary_name, pre + "_" + name + "_formlabel"
      entity.delete_attribute @dictionary_name, pre + "_" + name + "_units"
      entity.delete_attribute @dictionary_name, pre + "_" + name + "_error"
      entity.delete_attribute @dictionary_name, pre + "_" + name + "_formulaunits"
    end

    entity.delete_attribute @dictionary_name, name
    entity.delete_attribute @dictionary_name, "_" + name + "_formula"
    entity.delete_attribute @dictionary_name, "_" + name + "_label"
    entity.delete_attribute @dictionary_name, "_" + name + "_access"
    entity.delete_attribute @dictionary_name, "_" + name + "_options"
    entity.delete_attribute @dictionary_name, "_" + name + "_formlabel"
    entity.delete_attribute @dictionary_name, "_" + name + "_units"
    entity.delete_attribute @dictionary_name, "_" + name + "_error"
    entity.delete_attribute @dictionary_name, "_" + name + "_formulaunits"
  end

  # This method renames an attribute.
  #
  #   Args:
  #     entity: required, reference to the entity with the attribute
  #     name: required string, name of the attribute to rename
  #     new_name: required string, new name of the attribute
  #     do_recursive: optional boolean, if true, rename all subentities as well
  #     reference_string: optional string, if set, then only formula references
  #                       that begin with reference_string! will be replaced.
  #                       This allows for a match on Bob!Weight while skipping
  #                       Jim!Weight, even though Weight is the same name.
  #     formulas_only: optional boolean, if true then only do replacing inside
  #                    formulas, not to the actual attribute.
  #
  #   Returns:
  #     Nothing
  def rename_attribute(entity, name, new_name, new_label, do_recursive = false,
                                                          reference_string = nil, formulas_only = false)
    if entity.is_a?(Sketchup::Group)
      attribute_entity = entity
    else
      attribute_entity = entity.definition
    end
    for key in get_attribute_list(entity)
      formula = get_attribute_formula(entity, key)
      if formula != nil
        # TODO(scottlininger): Currently, it is possible to accidentally
        # search and replace references inside quotes strings, which is a bug.
        # For example, if a formula is ="That Bob is " & Bob & " feet tall",
        # then the system will replace both instances of the string Bob instead
        # only only replacing the actual formula reference. Our regular
        # expression match should be made smarter to avoid this.
        #
        # Note: The "true" param makes the regular expression case insensitive.
        if formulas_only == true
          rexp = Regexp.new("(" + reference_string + '\!)(' + name + ')(\W)', true)
          replaced_formula = formula.gsub(rexp, '\1' + new_label + '\3')
          rexp = Regexp.new("(" + reference_string + '\!)(' + name + ")$", true)
          replaced_formula = replaced_formula.gsub(rexp, '\1' + new_label)
        else
          rexp = Regexp.new('(^|[^\!])(' + name + ')(\W)', true)
          replaced_formula = formula.gsub(rexp, '\1' + new_label + '\3')
          rexp = Regexp.new('(^|[^\!])(' + name + ")$", true)
          replaced_formula = replaced_formula.gsub(rexp, '\1' + new_label)
        end
        set_attribute_formula(entity, key, replaced_formula)
      end
    end
    if formulas_only == false && get_attribute_label(entity, name) != nil
      set_attribute(entity, new_name,
                    get_attribute_value(entity, name),
                    get_attribute_formula(entity, name),
                    new_label,
                    get_attribute_access(entity, name),
                    get_attribute_options(entity, name),
                    get_attribute_formlabel(entity, name),
                    get_attribute_units(entity, name),
                    get_attribute_error(entity, name),
                    get_attribute_formulaunits(entity, name))
      delete_attribute(entity, name)
    end

    if entity.is_a?(Sketchup::Group)
      subentities = entity.entities
    else
      subentities = entity.definition.entities
    end
    if do_recursive == true
      for subentity in subentities
        if subentity.is_a?(Sketchup::Group) || subentity.is_a?(Sketchup::ComponentInstance)
          rename_attribute(subentity, name, new_name, new_label, true,
                           reference_string, true)
        end
      end
    end
  end

  # This method recursively "renames" all references to a given "sheet name"
  # inside a given entity. The sheet name is the bit that appears in a formula
  # before the exclamation point. So in "=MyEntity!LENX", the "MyEntity" is the
  # sheet name. It basically walks all of the formulas in an entity
  # and looks for 'oldName!' and replaces with 'newName!'
  #
  #   Args:
  #     entity: required, reference to the entity that is getting its formulas
  #             searched
  #     old_name: required string, old name of the "sheet"
  #     new_name: required string, new name of the "sheet"
  #
  #   Returns:
  #     Nothing
  def replace_formula_references(entity, old_name, new_name)
    if entity.is_a?(Sketchup::ComponentInstance)
      attribute_entity = entity.definition
    else
      attribute_entity = entity
    end

    # Replace all formula references to the old_name with new_name.
    for key in get_attribute_list(entity)
      formula = get_attribute_formula(entity, key)
      if formula != nil
        # TODO(scottlininger): The following regular expression could be made
        # smarter so that it does NOT match references that are in quoted
        # strings, so that a formula ="I Love MyPart! It's "&MyPart!MATERIAL
        # would not replace the MyPart! inside the quotes. However, this seems
        # like such an unusual occurence that I'm not worrying about it for
        # now.
        rexp = Regexp.new(old_name + '\!', true)
        replaced_formula = formula.gsub(rexp, new_name + "!")
        if formula != replaced_formula
          set_attribute_formula(entity, key, replaced_formula)
        end
      end
    end

    # Run the replacement recursively down the children.
    subentities = attribute_entity.entities
    for subentity in subentities
      if subentity.is_a?(Sketchup::Group) || subentity.is_a?(Sketchup::ComponentInstance)
        replace_formula_references(subentity, old_name, new_name)
      end
    end
  end

  # This method destroys all attributes. Called from the onExplode observer to
  # ensure that positioning info that makes no sense in a newly exploded state
  # is cleaned up.
  #
  #   Args:
  #     entity: required, reference to the entity that is getting fixed.
  #
  #   Returns:
  #     Nothing
  def delete_all_attributes(entity)
    attrdicts = entity.attribute_dictionaries
    if attrdicts
      attrdict = attrdicts["dynamic_attributes"]
      if attrdict
        attrdicts.delete(attrdict)
      end
    end

    if entity.is_a?(Sketchup::ComponentInstance)
      attrdicts = entity.definition.attribute_dictionaries
      if attrdicts
        attrdict = attrdicts["dynamic_attributes"]
        if attrdict
          attrdicts.delete(attrdict)
        end
      end
    end
  end

  # This method returns an ordered array of all attributes that are attached
  # to an entity. In the case of component instances, attributes attached to
  # both the instance and the definition will be returned.
  #
  #   Args:
  #     attribute_entity: required, reference to the entity to report on
  #     show_all: optional boolean, if true then attributes with name that start
  #               with an underscore will be listed as well
  #
  #   Returns:
  #     array of strings containing attribute names
  def get_attribute_list(attribute_entity, show_all = false, show_defaults = false)
    list = {}
    if attribute_entity.attribute_dictionaries
      if attribute_entity.attribute_dictionaries[@dictionary_name]
        dictionary = attribute_entity.attribute_dictionaries[@dictionary_name]
        for key in dictionary.keys
          if key[0..0] != "_" || show_all == true
            list[key] = second_if_empty(@default_sorts[key], key)
          end
        end
      end
    end
    if attribute_entity.is_a?(Sketchup::ComponentInstance)
      attribute_entity = attribute_entity.definition
      if attribute_entity.attribute_dictionaries
        if attribute_entity.attribute_dictionaries[@dictionary_name]
          dictionary = attribute_entity.attribute_dictionaries[@dictionary_name]
          for key in dictionary.keys
            if key[0..0] != "_"
              list[key] = second_if_empty(@default_sorts[key], key)
            elsif key[0..@instance_cache_prefix.length - 1] !=
                  @instance_cache_prefix
              # Check for formulas, since it's possible that a formula is set
              # but a value is not.
              formula_index = key.index(/_formula/)
              if formula_index == key.length - 8
                source_key = key[1..formula_index - 1]
                list[source_key] = second_if_empty(@default_sorts[source_key],
                                                   source_key)
              end
            end
          end
        end
      end
    end

    if show_defaults == true
      list["lenx"] = second_if_empty(@default_sorts["lenx"], 1000)
      list["leny"] = second_if_empty(@default_sorts["leny"], 1000)
      list["lenz"] = second_if_empty(@default_sorts["lenz"], 1000)
      list["x"] = second_if_empty(@default_sorts["x"], 1000)
      list["y"] = second_if_empty(@default_sorts["y"], 1000)
      list["z"] = second_if_empty(@default_sorts["z"], 1000)
      list["material"] = second_if_empty(@default_sorts["material"], 1000)
      # TODO(scottlininger): For now, MSRP is not a standard attribute, but
      # leave it in here for now until we determine what we're doing with this
      # attribute in the long term.
      # list['msrp'] = second_if_empty(@default_sorts['msrp'],1000)
    end

    # Sort the hash by value.
    return_list = []
    nested_array = list.sort { |a, b|

      # This converts our sort values to strings so
      # we can sort a number like 10 against "SomeName"
      if a[1].to_i == a[1]
        a[1] = a[1].to_s.rjust(20, "0")
      end
      if b[1].to_i == b[1]
        b[1] = b[1].to_s.rjust(20, "0")
      end
      a[1].casecmp(b[1])
    }
    for sub_array in nested_array
      return_list.push(sub_array[0])
    end
    return return_list
  end

  # This method looks for an attribute first on the instance. If the attribute
  # is found there, then we return it. Otherwise, we look for the same attribute
  # on the definition. This allows for an "instance trumps definition" model
  # where we could extend DCs in the future to fully understand SketchUp's
  # way of understanding instancing.
  #
  #   Args:
  #      entity: reference to the entity to get the value from
  #      name: string name of the attribute to return the value for
  #
  #   Returns:
  #      the value of the attribute, or nil if it can't determine that
  def get_cascading_attribute(entity, name)
    name = name.downcase

    if entity.is_a?(Sketchup::ComponentInstance)
      value = entity.get_attribute(@dictionary_name, name)
      if value == nil
        value = entity.definition.get_attribute @dictionary_name, name
      end
      return value
    elsif entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::Model) ||
          entity.is_a?(Sketchup::ComponentDefinition)
      return entity.get_attribute(@dictionary_name, name)
    else
      return nil
    end
  end

  # This method pulls an attribute from an entity's definition. If the entity
  # is a group, then it pulls it from the group itself.
  #
  #   Args:
  #      entity: reference to the entity to get the value from
  #      name: string name of the attribute to return the value for
  #
  #   Returns:
  #      the value of the attribute, or nil if it can't determine that
  def get_definition_attribute(entity, name)
    name = name.downcase

    if entity.is_a?(Sketchup::ComponentInstance)
      value = entity.definition.get_attribute(@dictionary_name, name)
      return value
    elsif entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::Model) ||
          entity.is_a?(Sketchup::ComponentDefinition)
      return entity.get_attribute(@dictionary_name, name)
    else
      return nil
    end
  end

  # This method pulls an attribute from an entity's instance. If the entity
  # is a group, then it pulls it from the group itself.
  #
  #   Args:
  #      entity: reference to the entity to get the value from
  #      name: string name of the attribute to return the value for
  #
  #   Returns:
  #      the value of the attribute, or nil if it can't determine that
  def get_instance_attribute(entity, name)
    name = name.downcase

    if entity.is_a?(Sketchup::ComponentInstance) || entity.is_a?(Sketchup::Group)
      value = entity.get_attribute(@dictionary_name, name)
      return value
    else
      return nil
    end
  end

  # This method deletes a specific attribute at both the instance and the
  # definition level. (This is "less powerful" than delete_attribute, which
  # removes an attribute and all of its metaattributes.)
  #
  #   Args:
  #      entity: reference to the entity to get the value from
  #      name: string name of the attribute to delete
  #
  #   Returns:
  #      nothing
  def delete_cascading_attribute(entity, name)
    name = name.downcase
    entity.delete_attribute(@dictionary_name, name)
    if entity.is_a?(Sketchup::ComponentInstance)
      entity.definition.delete_attribute(@dictionary_name, name)
    end
  end

  # This method deletes any attributes that have been attached that
  # were storing animation state. These attributes are created in dctools.rb,
  # and have names like "_onclick_state1", "_onclick_state2", etc.
  #
  #   Args:
  #      entity: reference to the entity to remove attributes from
  #
  #   Returns:
  #      nothing
  def delete_onclick_state(entity)
    for key in get_attribute_list(entity, true)
      if key.index(/_onclick_state/) == 0
        delete_cascading_attribute(entity, key)
      end
    end
  end

  # This method returns a formula result if a formula is present. If no formula
  # is set, then it returns the get_live_value if that exists. Otherwise it will
  # return the stored value if that exists. If all of these fail, it return nil.
  #
  #   Args:
  #      entity: reference to the entity to get the value from
  #      name: string name of the attribute to return the value for
  #
  #   Returns:
  #      the value of the attribute, or nil if it can't determine that
  def get_attribute_value(entity, name)
    name = name.downcase
    if get_attribute_formula(entity, name) != nil
      return get_cascading_attribute(entity, name)
    end
    value = get_live_value(entity, name)
    if value == nil
      return get_cascading_attribute(entity, name)
    end
    return value
  end

  # This method returns an attribute's formula if a formula is present.
  #
  #   Args:
  #      entity: reference to the entity to get the formula from
  #      name: string name of the attribute to return the formula for
  #
  #   Returns:
  #      string: the formula of the attribute, or nil if it can't determine that
  def get_attribute_formula(entity, name)
    return get_cascading_attribute(entity, "_" + name + "_formula")
  end

  # This method returns an attribute's label if a label is present.
  #
  #   Args:
  #      entity: reference to the entity to get the label from
  #      name: string name of the attribute to return the label for
  #
  #   Returns:
  #      string: the label of the attribute, or nil if it can't determine that
  def get_attribute_label(entity, name)
    return get_cascading_attribute(entity, "_" + name + "_label")
  end

  # This method returns an attribute's units if defined.
  #
  #   Args:
  #      entity: reference to the entity to get the units from
  #      name: string name of the attribute to return the units for
  #
  #   Returns:
  #      string: the units of the attribute, or nil if it can't determine that
  def get_attribute_units(entity, name)
    return get_cascading_attribute(entity, "_" + name + "_units")
  end

  # This method returns an attribute's formlabel if defined. The formlabel is
  # the "friendly name" that end users will see if they configure the component.
  #
  #   Args:
  #      entity: reference to the entity to get the formlabel from
  #      name: string name of the attribute to return the formlabel for
  #
  #   Returns:
  #      string: the formlabel of the attribute, or nil if it can't determine that
  def get_attribute_formlabel(entity, name)
    return get_cascading_attribute(entity, "_" + name + "_formlabel")
  end

  # This method returns an attribute's access setting if a label is present.
  # The access setting for each attribute is how SketchUp determines what
  # to show in the Configure window for "consumers," i.e. people who are
  # using the component instead of creating it.
  #
  # The list of valid access settings include:
  #   - "NONE" if consumers can't see or edit this attribute
  #   - "VIEW" if consumers can see this attribute
  #   - "TEXTBOX" if consumer can enter a value for this attribute
  #   - "LIST" if consumers can select a value from a list
  #
  #   Args:
  #      entity: reference to the entity to get the access setting from
  #      name: string name of the attribute to return the access setting for
  #
  #   Returns:
  #      string: the access level of the attribute, or nil if not set
  def get_attribute_access(entity, name)
    return get_cascading_attribute(entity, "_" + name + "_access")
  end

  # This method returns an attribute's options list if it is present. This list
  # is a ampersand separated string of name/value pairs that looks like a
  # web query string. It comes into play if the access setting for an attribute
  # is set to "LIST."
  #
  # EXAMPLE OPTIONS STRING: 'Small=10&Medium=15&Really+Large=25'
  #
  # See get_attribute_access for more information.
  #
  #   Args:
  #      entity: reference to the entity to get the options from
  #      name: string name of the attribute to return the options for
  #
  #   Returns:
  #      string: the options of the attribute, or nil if it can't determine that
  def get_attribute_options(entity, name)
    return get_cascading_attribute(entity, "_" + name + "_options")
  end

  # This method returns an attribute's formula error if present.
  #
  # EXAMPLE ERROR STRING: 5+<span class="subformula-error">UnfoundVar</span>
  #
  #   Args:
  #      entity: reference to the entity to get the error from
  #      name: string name of the attribute to return the error for
  #
  #   Returns:
  #      string: the error of the attribute, or nil if it can't determine that
  def get_attribute_error(entity, name)
    return get_cascading_attribute(entity, "_" + name + "_error")
  end

  # This method returns an attribute's formulaunits if defined.
  #
  #   Args:
  #      entity: reference to the entity to get the units from
  #      name: string name of the attribute to return the formulaunits for
  #      return_defaults: optional boolean, if true then determine the default
  #        units that are attached, assuming it's a reserved attribute. If not
  #        a reserved attribute, return 'STRING'
  #   Returns:
  #      string: the units of the attribute, or nil if it can't determine that
  def get_attribute_formulaunits(entity, name, return_defaults = false)
    name = name.downcase
    formulaunits = get_cascading_attribute(entity, "_" + name + "_formulaunits")
    if return_defaults == false
      return formulaunits
    elsif formulaunits != nil
      return formulaunits
    elsif @conv.reserved_attribute_group[name] != nil
      group = @conv.reserved_attribute_group[name]
      if group == "LENGTH"
        length_units = get_entity_lengthunits(entity)
        if length_units != nil
          return length_units
        else
          return "INCHES"
        end
      else
        return @conv.groups_hash[group]["base"]
      end
    else
      return "STRING"
    end
  end

  # This method returns an entity's "length units", as stored on an attribute
  # called _lengthunits. This attribute determines which formulaunits to
  # assume that all length attributes are stored in on that entity.
  #
  #   Args:
  #      entity: reference to the entity to get the length units from
  #      use_default: boolean, if true, then we will default empty results
  #        to the appropriate units.
  #
  #   Returns:
  #      string: the units of the attribute, or nil if unset
  def get_entity_lengthunits(entity, use_default = false)
    length_units = get_attribute_value(entity, "_lengthunits")
    if length_units == nil && use_default == true
      # If we have an attribute dictionary, then we are an old component and
      # we should default to "INCHES". Otherwise, default to the model units.
      if has_dc_attribute_dictionary(entity)
        length_units = "INCHES"
      else
        length_units = get_default_authoring_units()
      end
    end
    return length_units
  end

  # This method returns an attribute's base units.
  #
  #   Args:
  #      entity: reference to the entity to get the base units from
  #      name: string name of the attribute to return the formulaunits for
  #
  #   Returns:
  #      string: the base units of the attribute, or 'STRING' if unclear
  def get_attribute_base_units(entity, name)
    formulaunits = get_attribute_formulaunits(entity, name, true)
    return @conv.units[formulaunits]["base"]
  end

  # This method takes a query string and parses it into a hash with the same
  # name/value pairs as the query string
  #
  #     'Small=10&Medium=15&Really+Large=25'
  #
  # The above query string would be parsed into a hash like so...
  #
  #     hash['Small'] = '10'
  #     hash['Medium'] = '15'
  #     hash['Really Large'] = '25'
  #
  #   Args:
  #      query: a string with url encoded data, separated by '&'s
  #
  #   Returns:
  #      hash: the same data from the query string in hash form
  def query_to_hash(dialog, query)
    param_pairs = query.split("&")
    param_hash = {}
    for param in param_pairs
      name, value = param.split("=")
      name = unescape(name)
      value = unescape(value)
      param_hash[name] = value
    end

    # Ruby/JS Bridge uses queryid as the ID of an element containing the
    # real query data. In most cases the queryid will be the only parameter
    # but in either case this will overwrite any values with what's found in
    # the bridge's query element value
    query_id = param_hash["queryid"]
    if (query_id != nil && query_id != "")
      query_str = dialog.get_element_value(query_id)
      param_pairs = query_str.split("&")
      for param in param_pairs
        name, value = param.split("=")
        name = unescape(name)
        value = unescape(value)
        param_hash[name] = value
      end
    end
    return param_hash
  end

  # URL-encode a string. Note the second parameter, which allows you to say
  # that you prefer the escape spaces (' ') with a plus character instead
  # of %20. This is significant because certain browsers have trouble with
  # one encoding vs. the other.
  #
  #   url_encoded_string = CGI::escape("'Stop!' said Fred")
  #      # => "%27Stop%21%27+said+Fred"
  def escape(string, replace_spaces_with_plus = true)
    if replace_spaces_with_plus == true
      space_string = "+"
    else
      space_string = "%20"
    end
    if string != nil
      string = url_escape(string, space_string)
    end
    return string
  end

  # URL-decode a string.
  #   string = CGI::unescape("%27Stop%21%27+said+Fred")
  #      # => "'Stop!' said Fred"
  def unescape(string)
    if string != nil
      string = url_unescape(string)
    end
    return string
  end

  if RUBY_VERSION.to_i < 2
    # Ruby 1.8

    # URL-encode a string.
    #   url_encoded_string = CGI::escape("'Stop!' said Fred")
    #      # => "%27Stop%21%27+said+Fred"
    def url_escape(string, space = "+")
      string.gsub(/([^ a-zA-Z0-9_.]+)/n) do
        "%" + $1.unpack("H2" * $1.size).join("%").upcase
      end.gsub(/ /, space)
    end

    # URL-decode a string.
    #   string = CGI::unescape("%27Stop%21%27+said+Fred")
    #      # => "'Stop!' said Fred"
    def url_unescape(string)
      string.tr("+", " ").gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
        [$1.delete("%")].pack("H*")
      end
    end
  else
    # Ruby 2.0

    # URL-encode a string.
    #   url_encoded_string = CGI::escape("'Stop!' said Fred")
    #      # => "%27Stop%21%27+said+Fred"
    def url_escape(string, space = "+")
      encoding = string.encoding
      string.b.gsub(/([^ a-zA-Z0-9_.]+)/) do |m|
        "%" + m.unpack("H2" * m.bytesize).join("%").upcase
      end.gsub(/ /, space).force_encoding(encoding)
    end

    # URL-decode a string with encoding(optional).
    #   string = CGI::unescape("%27Stop%21%27+said+Fred")
    #      # => "'Stop!' said Fred"
    def url_unescape(string, encoding = "UTF-8")
      str = string.tr("+", " ").b.gsub(/((?:%[0-9a-fA-F]{2})+)/) do |m|
        [m.delete("%")].pack("H*")
      end.force_encoding(encoding)
      str.valid_encoding? ? str : str.force_encoding(string.encoding)
    end
  end # if RUBY_VERSION

  # Cleans up strings to inclusion inside JSON string values
  #
  #   Args:
  #      value: a string that we want escaped
  #
  #   Returns:
  #      string: a JSON-friendly version suitable for parsing in javascript
  def clean_for_json(value)
    value = value.to_s

    # JSON escape characters
    value = value.gsub(/\\/, "&#92;")
    value = value.gsub(/\//, "&#47;")
    value = value.gsub(/\"/, "&quot;")
    value = value.gsub(/\r\n?/, '\n')
    value = value.gsub(/\n/, '\n')
    value = value.gsub(/\r/, '\r')
    value = value.gsub(/\t/, '\t')

    # Get rid of extraneous characters at conversion time
    value = value.gsub(/\b/, "")
    value = value.gsub(/\f/, "")
    if value.index(/e-\d\d\d/) == value.length - 5
      value = "0.0"
    end

    return value
  end

  # Cleans up strings for inclusion inside XML structure. Replaces problematic
  # characters with their XML escaped version.
  #
  #   Args:
  #      value: a string that we want escaped
  #
  #   Returns:
  #      string: an xml-friendly version suitable for parsing
  def clean_for_xml(value)
    value = value.to_s
    value = value.gsub(/\</, "&lt;")
    value = value.gsub(/\>/, "&gt;")
    value = value.gsub(/\"/, "&quot;")
    value = value.gsub(/\'/, "&apos;")
    return value
  end

  # Creates a JSON string respresentation of an entity and its children
  #
  #   Args:
  #      entity: the entity to convert
  #      do_recursion: boolean stating whether to pull in children as well
  #      specific_dictionary: optional string name of the attribute dictionary
  #        to output. If this is set, then the JSON is guaranteed to have an
  #        entry for this dictionary, even if the dictionary doesn't exist on
  #        the entity. If it is nil, then we will output all dictionaries found.
  #      nest_meta_attributes: optional boolean stating whether to return meta
  #        attributes (such as "_x_formula") as friendly parsed "nested"
  #        subvariables rather than raw name, value pairs. Defaults to true.
  #      current_depth: optional integer that tracks the current depth of the
  #        entity from the root. The default is zero.
  #      max_recusion_depth: optional integer that determines when recursion
  #        should stop. The default is one, which will deliver the current
  #        entity and its children. Use -1 to build the entire tree.
  #
  #   Returns:
  #      string: a JSON string ready for parsing with javascript
  def entity_to_json(entity, do_recursion = true,
                             specific_dictionary = nil, nest_meta_attributes = true,
                             current_depth = 0, max_recursion_depth = 1)

    # If we're at the top of a tree, create a hash to keep track of names
    # inside the tree. We'll use this to generate unique names for those
    # elements with overlapping names.
    if @has_used_name == nil
      @has_used_name = {}
      clear_names_before_return = true
    else
      clear_names_before_return = false
    end

    # generate basic entity json
    json = "{"

    # Cache the entity in `entity_pointers` if it is a Sketchup::Entity.
    # NOTE(Isaac): Sometimes this method is called and entity is actually a
    # Sketchup::Model.
    if entity.is_a?(Sketchup::Entity)
      @entity_pointers[entity.entityID.to_s] = entity
    end

    # If this JSON is being constructed in a DC context (meaning we're
    # requesting an entity's JSON for purposes of applying formulas, etc.) then
    # get the entity's name that is "safe" for the parser.
    if specific_dictionary == @dictionary_name
      this_name = get_entity_formula_name(entity)
    else
      this_name = entity.name
    end

    # Record the fact that we've used this name, so that other members in the
    # tree can avoid colliding with it.
    @has_used_name[this_name] = true

    json += '"name": "' + clean_for_json(this_name) + '", '
    json += '"typename": "' + entity.typename + '", '
    json += '"id": "' + entity.entityID.to_s + '", '

    if entity.is_a?(Sketchup::ComponentInstance)
      json += '"guid": "' + clean_for_json(entity.definition.guid) + '", '
      json += '"definitionID": "' +
              clean_for_json(entity.definition.entityID) + '", '
    elsif entity.is_a?(Sketchup::ComponentDefinition) || entity.is_a?(Sketchup::Model)
      json += '"guid": "' + clean_for_json(entity.guid) + '", '
    end

    if entity.is_a?(Sketchup::ComponentDefinition) ||
       entity.is_a?(Sketchup::Model) || entity.is_a?(Sketchup::Group)
      json += '"description": "' + clean_for_json(entity.description) + '", '
    end

    # Add the 3D Warehouse id when available.
    if entity.is_a?(Sketchup::ComponentDefinition) ||
       entity.is_a?(Sketchup::Model)
      model_id = entity.get_attribute("3DWarehouse", "ModelID")
      if !model_id.nil?
        json += '"warehouseId": "' + clean_for_json(model_id) + '", '
      end
    end

    # Add `layer`, `volume` and other default attributes.
    json += '"layer": "' + clean_for_json(entity.layer.name) + '", '

    if entity.is_a?(Sketchup::ComponentInstance) ||
       entity.is_a?(Sketchup::Group)
      volume = fix_float(entity.volume)
      if volume >= 0
        json += '"volume": "' + volume.to_s + '", '
      end
    end

    for attribute_name in ["X", "Y", "Z", "LenX", "LenY", "LenZ", "Material"]
      value = get_live_value(entity, attribute_name)
      # puts "get_live_value value 👉 #{value}"
      if !value.nil?
        json += '"' + attribute_name + '": "' + value.to_s + '", '
      end
    end

    # Add classifier data.
    classifier_data = get_classifier_data(entity)
    json += '"classifier": { '
    classifier_data.each do |column, value|
      json += '"' + clean_for_json(column) + '":"' +
              clean_for_json(value) + '",'
    end
    json = json.chop + "}, "

    # This is needed for the display of CSS files that have a relative path.
    # For example, if I enter a CSSURL of "mycss.css", then the configure
    # dialog will use this file variable to calculate a local path to the file.
    json += '    "file": "' + clean_for_json(Sketchup.active_model.path) + '", '

    if entity.is_a?(Sketchup::Model)
      shadow_info = Sketchup.active_model.shadow_info
      lat = second_if_empty(shadow_info["Latitude"].to_f, 0.0)
      lng = second_if_empty(shadow_info["Longitude"].to_f, 0.0)
      json += '"latitude": "' + clean_for_json(lat) + '", '
      json += '"longitude": "' + clean_for_json(lng) + '", '
      json += '"bounds": {'
      json += '"width":"' + clean_for_json(entity.bounds.width.to_f) + '", '
      json += '"depth":"' + clean_for_json(entity.bounds.depth.to_f) + '", '
      json += '"height":"' + clean_for_json(entity.bounds.height.to_f) + '"}, '
    end

    if entity.is_a?(Sketchup::ComponentDefinition)
      json += '"countInstances": "' + clean_for_json(entity.count_instances) +
              '", '
    end

    if entity.is_a?(Sketchup::ComponentDefinition)
      # Generate json for new attributes
      attrdict = entity.attribute_dictionary("SU_DefinitionSet")
      if !attrdict.nil?
        json += attribute_dictionary_to_json(attrdict)
        json += ", "
      end
    end

    if entity.is_a?(Sketchup::ComponentInstance)
      # Generate json for new attributes
      attrdict = entity.definition.attribute_dictionary("SU_DefinitionSet")
      if !attrdict.nil?
        json += attribute_dictionary_to_json(attrdict)
        json += ", "
      end

      attrdict = entity.attribute_dictionary("SU_InstanceSet")
      if !attrdict.nil?
        json += attribute_dictionary_to_json(attrdict)
        json += ", "
      end
    end

    json += ' "attributeDictionaries": { '

    # Determine which attribute dictionaries to include in the JSON.
    dictionaries_to_output = {}

    # Construct a hash whose keys contain the names of the attribute
    # dictionaries we need to output.
    if specific_dictionary != nil
      dictionaries_to_output[specific_dictionary] = true
    else
      if entity.attribute_dictionaries != nil
        for dict in entity.attribute_dictionaries
          dictionaries_to_output[dict.name] = true
        end
      end

      # If the entity is a Component, show its definition's attributes as well.
      if entity.is_a?(Sketchup::ComponentInstance)
        if entity.definition.attribute_dictionaries != nil
          for dict in entity.definition.attribute_dictionaries
            dictionaries_to_output[dict.name] = true
          end
        end
      end
    end

    for dict in dictionaries_to_output.keys
      if nest_meta_attributes == true && dict == @dictionary_name

        # Dynamic Component Attributes.
        json += '"' + clean_for_json(dict) + '": {'
        for key in get_attribute_list(entity)
          json += '"' + key + '": {'
          json += '"value":"' + clean_for_json(get_attribute_value(entity, key)) + '"'
          if get_attribute_formula(entity, key) != nil
            json += ',"formula":"' + clean_for_json(get_attribute_formula(entity, key)) + '"'
          end
          if get_attribute_label(entity, key) != nil
            json += ',"label":"' + clean_for_json(get_attribute_label(entity, key)) + '"'
          end
          if get_attribute_options(entity, key) != nil
            json += ',"options":"' + clean_for_json(get_attribute_options(entity, key)) + '"'
          end
          if get_attribute_access(entity, key) != nil
            json += ',"access":"' + clean_for_json(get_attribute_access(entity, key)) + '"'
          end
          if get_attribute_formlabel(entity, key) != nil
            json += ',"formlabel":"' + clean_for_json(get_attribute_formlabel(entity, key)) + '"'
          end
          if get_attribute_units(entity, key) != nil
            json += ',"units":"' + clean_for_json(get_attribute_units(entity, key)) + '"'
          end
          if get_attribute_error(entity, key) != nil
            json += ',"error":"' + clean_for_json(get_attribute_error(entity, key)) + '"'
          end
          if get_attribute_formulaunits(entity, key) != nil
            json += ',"formulaunits":"' +
                    clean_for_json(get_attribute_formulaunits(entity, key)) + '"'
          end
          json += "}, "
        end

        # Pull in the length units attached to this entity. If not set, default
        # to inches if the component contains any attributes at all, or to the
        # default authoring units based on the user's model.
        length_units = get_entity_lengthunits(entity, true)

        json += '"_iscollapsed": {"value":"' +
                clean_for_json(get_attribute_value(entity, "_iscollapsed")) + '"},' +
                '"_lengthunits": {"value":"' +
                clean_for_json(length_units) + '"},' +
                '"_name": {"value":"' +
                clean_for_json(get_attribute_value(entity, "_name")) + '"}}, '
      elsif Sketchup.is_pro?
        # Classifier Attributes.
        json += '"' + clean_for_json(dict) + '": {'
        if entity.is_a?(Sketchup::ComponentInstance)
          attrdict = entity.definition.attribute_dictionary dict
        else
          attrdict = entity.attribute_dictionary dict
        end

        json += attribute_dictionary_to_json(attrdict)
        json += "}, "
      end
    end
    json = json.gsub(/, $/, "")
    json += "}, "

    # Generate json for subentities.
    if entity.is_a?(Sketchup::ComponentInstance)
      subentities = entity.definition.entities
    else
      subentities = entity.entities
    end

    # This method can continue recursion if max_recursion_depth == -1 or if
    # the next depth is less than or equal to the max_recursion_depth.
    can_go_deeper = (max_recursion_depth == -1)
    if !can_go_deeper
      can_go_deeper = current_depth + 1 <= max_recursion_depth
    end

    if subentities != nil && do_recursion.to_s == "true" && can_go_deeper
      json += '"subentities":['
      for subentity in subentities
        if subentity.is_a?(Sketchup::ComponentInstance) ||
           subentity.is_a?(Sketchup::Group)
          # Never send up json for our "highlight instance"
          if copy_number_of(subentity) == 0.0 &&
             subentity.name != "(highlight)"
            json += entity_to_json(subentity, do_recursion, specific_dictionary,
                                   nest_meta_attributes, current_depth + 1, max_recursion_depth)
            json += ", "
          end
        end
      end
      json = json.gsub(/, $/, "")
      json += "], "
    end

    # If we're requesting a json struct for the entire model, then pull in
    # definitions attributes.
    if entity.is_a?(Sketchup::Model)
      json += '"definitions":['
      for definition in entity.definitions
        json += entity_to_json(definition, false, specific_dictionary,
                               nest_meta_attributes) + ", "
      end
      json = json.gsub(/, $/, "")
      json += "], "
    end

    json = json.gsub(/, $/, "")
    json += "}"

    # If we're at the top level of a tree, clear out our names hash
    # before returning.
    if clear_names_before_return == true
      @has_used_name = nil
      #puts json
    end
    return json
  end

  # Outputs an entire AttributeDictionary to JSON suitable for sending through
  # the bridge.
  #
  #   Args:
  #      attrdict: the AttributeDictionary to export
  #
  #   Returns:
  #      string: the json string
  def attribute_dictionary_to_json(attrdict)
    json = ""
    if display_attribute?(attrdict, @simplify)
      attrdict.each_pair { |key, value|
        next if key.nil? || key.empty?
        if value.kind_of?(Array)
          # We must use `.inspect` on arrays in order to ensure we get the
          # correct formatting under both Ruby 1.8 and Ruby 2.0.
          json << %["#{clean_for_json(key)}":#{value.inspect}, ]
        else
          json << %["#{clean_for_json(key)}":"#{clean_for_json(value)}", ]
        end
      }
      if attrdict.attribute_dictionaries
        attrdict.attribute_dictionaries.each { |sub_dict|
          next if !display_attribute?(sub_dict, @simplify)
          sub_json = attribute_dictionary_to_json(sub_dict)
          next if sub_json.empty?
          json << %["#{sub_dict.name}":{#{sub_json}}, ]
        }
      end

      json = json.gsub(/, $/, "")
    end
    return json
  end

  # Determines if a classifier attribute should be displayed or not.
  #
  #   Args:
  #      attrdict: the AttributeDictionary representing the classifier attribute
  #
  #   Returns:
  #      boolean:
  def display_attribute?(dictionary, simplify = true)
    return false if dictionary.nil?
    if simplify
      # Is flag is set by SketchUp based on the filter lists.
      return false if dictionary["is_hidden"]
    end
    # We do not currently display 'choice' attributes as these would require
    # attaching new attributes based on the selection.
    return false if dictionary["attribute_type"] == "choice"
    return true
  end

  # Parses the attribute path returned from WebDialogs where each component
  # in the path is separated with a period.
  #
  # However, a period may appear in the schema or attribute name, so there might
  # be escaped periods: \.
  #
  # Example:
  #   parse_attribute_path('gbXML5\.10.controlZoneIdRef.value')
  #   # => ["gbXML5.10", "controlZoneIdRef", "value"]
  #
  #   Args:
  #      path: a dot-notation path to the attribute I want to set, like
  #            "favorite.foods.desserts.pie"
  #
  #   Returns:
  #      array: Each component of the path unescaped.
  def parse_attribute_path(path)
    components = [""]
    # Doing a regex split will work with Unicode strings on Ruby 1.8 and 2.0.
    chars = path.split(//)
    escape = false
    for char in chars
      if !escape && char == "."
        components << ""
        escape = false
      elsif !escape && char == '\\'
        escape = true
      else
        components.last << char
        escape = false
      end
    end
    return components
  end

  # Sets a nested attribute on an entity, meaning an attribute that is stored
  # in AttributeDictionaries within AttributeDictionaries. If the nested
  # path of AttributeDictionaries does not exist, it will be created so the
  # attribute can be set.
  #
  # Example usage:
  # dc_plugin.set_nested_attribute(some_component,
  #                                "favorite.foods.desserts.pie",
  #                                "cherry")
  #
  #   Args:
  #      entity: the Entity I want to set the attribute on.
  #      path: a dot-notation path to the attribute I want to set, like
  #            "favorite.foods.desserts.pie"
  #      value: the value I want to set, like "cherry".
  #
  #   Returns:
  #      None
  def set_nested_attribute(entity, path, value)
    dictionary_names = parse_attribute_path(path)
    attribute_name = dictionary_names.pop()

    if dictionary_names.length == 0
      raise 'path must have at least two dot-separated names, like "foo.bar"'
      return
    end

    for dictionary_name in dictionary_names
      if dictionary_name.length == 0
        raise 'path must have at least two dot-separated names, like "foo.bar"'
        return
      end
    end

    next_in_path = entity
    temp_attribute_name = "__DCTEMP__"
    for dictionary_name in dictionary_names
      # The only way in the Ruby API to create an AttributeDictionary is to
      # add an attribute to it. So we must do that for each step in our path.
      next_in_path.set_attribute(dictionary_name, temp_attribute_name, "")
      next_in_path = next_in_path.attribute_dictionaries[dictionary_name]
      next_in_path.delete_key(temp_attribute_name)
    end

    next_in_path[attribute_name] = value
  end

  # Gets a nested attribute on an entity, meaning an attribute that is stored
  # in AttributeDictionaries within AttributeDictionaries.
  # Example usage:
  # dc_plugin.get_nested_attribute(some_component,
  #                                "favorite.foods.desserts.pie")
  #
  #   Args:
  #      entity: the Entity I want to set the attribute on.
  #      path: a dot-notation path to the attribute I want to set, like
  #            "favorite.foods.desserts.pie"
  #
  #   Returns:
  #      value: The value of the attribute, or nil if it does not exist.
  def get_nested_attribute(entity, path)
    dictionary_names = parse_attribute_path(path)
    attribute_name = dictionary_names.pop()

    if dictionary_names.length == 0
      raise 'path must have at least two dot-separated names, like "foo.bar"'
      return
    end

    for dictionary_name in dictionary_names
      if dictionary_name.length == 0
        raise 'path must have at least two dot-separated names, like "foo.bar"'
        return
      end
    end

    next_in_path = entity

    for dictionary_name in dictionary_names
      next_in_path = next_in_path.attribute_dictionaries[dictionary_name]
      if next_in_path == nil
        return nil
      end
    end

    return next_in_path[attribute_name]
  end

  # Look at a givn entity to determine its DC-centric name, meaning the name
  # that one refers to it by when writing a formula. Note that an entity's
  # formula name can and often is seperate from its definition or instance
  # name. It's perfectly legal to have a definition called "Table" with
  # and instance called "MyTable" and a formula name of "Blue Table". Any
  # formulas that refer to the table would need to say "Blue Table!X", for
  # example. That being said, this method will construct a name *from* an
  # entity's definition or instance name if no explicit formula name is
  # stored in its _name attribute.
  #
  #   Args:
  #      entity: entity to find the name for.
  #
  #   Returns:
  #      string: the clean name
  def get_entity_formula_name(entity)

    # Look for a cached _name on the instance first.
    this_name = get_instance_attribute(entity, "_name")

    # Clean up the name to avoid parser problems. This is needed here because
    # it's possible that older versions of the DC plugin have allowed mis-named
    # components to be created, so we can capture and fix that here.
    if this_name.to_s != ""
      this_name = clean_entity_name(this_name)
    end

    # If no name was found, look for the SU-level name of the item.
    if this_name == "" || this_name == nil
      this_name = entity.name
      this_name = clean_entity_name(this_name)
    end

    # If still no name, then look for a cached _name on the definition.
    if this_name == "" || this_name == nil
      this_name = get_definition_attribute(entity, "_name")
    end

    # If still no name, then create one from the definition name or the typename
    # of the entity.
    if this_name == "" || this_name == nil
      if entity.is_a?(Sketchup::ComponentInstance)
        this_name = entity.definition.name
      else
        this_name = entity.typename
      end
      this_name = clean_entity_name(this_name)

      # If we're still empty (meaning the name from SU was nothing but invalid
      # characters, then default to the typename.
      if this_name == "" || this_name == nil
        this_name = entity.typename
      end
    end
    return this_name
  end

  # Clean up a given entity name so that it does not interfere with the formula
  # parser.
  #
  #   Args:
  #      this_name: Name to clean up
  #
  #   Returns:
  #      string: the clean name
  def clean_entity_name(this_name)

    # Strip off leading and trailing whitespace
    this_name = this_name.gsub(/^\s+/, "")
    this_name = this_name.gsub(/\s+$/, "")

    # Clean out any characters that are formula operators from the name.
    this_name = this_name.gsub(/[\+\-\*\/\(\)\<\>\=\^\!]/, "")

    # Avoid names that cause problems with DC formula parsing.
    if this_name.downcase == "model" || this_name.downcase == "parent"
      this_name += "Entity"
    end

    # Limit the length of the name to a reasonable maximum.
    this_name = this_name[0..MAX_ENTITY_NAME_LENGTH]

    return this_name
  end

  # Translate a string using the LangHandler object. Expects a global var
  # called $dc_strings (created in dynamiccomponents.rb)
  #
  #   Args:
  #      string_to_translate: UI string
  #
  #   Returns:
  #      string: nicely translated into the user's language by LangHandler
  def translate(string_to_translate)
    $dc_strings.GetString(string_to_translate)
  end

  # Take a spreadsheet reference contained in a formula and return its value.
  #
  # EXAMPLE: a reference such as "MyPart!X" might return the x position of a
  # Group called "MyPart".
  #
  #   Args:
  #      source_entity: pointer to the entity that holds the original formula
  #      reference: string reference we're trying to parse
  #      source_attribute: string name of the attribute we're parsing for
  #
  #   Returns:
  #      value: the value that was found, or 0.0 if it fails
  #      error: a marked up version of the reference
  def parse_reference(source_entity, reference, source_attribute)
    original_cased_reference = reference
    reference = reference.downcase
    if reference.index("!")
      sheet_name = reference[0..reference.index("!") - 1]
      attribute_name = (reference[reference.index("!") + 1..999]).downcase
      target_entity = find_entity_by_sheet_name(source_entity, sheet_name)
      if target_entity
        run_formula(target_entity, attribute_name.downcase)
        value = get_attribute_value(target_entity, attribute_name)

        # If the value is nil, then that means the attribute wasn't found or
        # contains nothing, so show the user a formula error.
        if value == nil
          return 0.0, "<span class=subformula-error>" + reference + "</span>"
        end

        value = convert(value, target_entity, attribute_name,
                        source_entity, source_attribute)

        return value, "<span class=subformula-success>" + value.to_s + "</span>"
      else
        puts "failed to parse " + sheet_name + "." + reference + " inside " +
               get_entity_formula_name(source_entity) + "." + source_attribute.to_s
        return 0.0, "<span class=subformula-error>" + reference + "</span>"
      end
    else
      target_entity = source_entity

      if reference == "true"
        return 1.0, "<span class=subformula-success>1.0</span>"
      elsif reference == "false"
        return 0.0, "<span class=subformula-success>0.0</span>"
      elsif (reference[0..0] == '"') &&
            (reference[reference.length - 1..reference.length - 1] == '"')
        return chop_quotes(original_cased_reference), "<span class=subformula-success>" + original_cased_reference + "</span>"
      elsif get_attribute_value(source_entity, reference.downcase) != nil
        result, error = get_formula_result(source_entity, reference.downcase, true)
        if result != nil
          value = result
          value = convert(result, target_entity, reference.downcase,
                          source_entity, source_attribute)

          return value, "<span class=subformula-success>" + value.to_s + "</span>"
        elsif error.to_s.include? "subformula-error"
          return nil, error
        else
          value = get_attribute_value(source_entity, reference.downcase)
          value = convert(value, target_entity, reference.downcase,
                          source_entity, source_attribute)
          return value, "<span class=subformula-success>" + value.to_s + "</span>"
        end
      else
        return 0.0, "<span class=subformula-error>" + reference.to_s + "</span>"
      end
    end
  end

  # This method takes a "sheet name" that was taken from a formula, and tries
  # to determine an actual entity that it references. The term "sheet" comes
  # from the spreadsheet world, where you refer to a given spreadsheet by name
  # through an exclamation point (!) notation. Dynamic Components provide a
  # miniature spreadsheet that is attached to each Group or Component Instance
  # in your Dynamic Component.
  #
  # For example, if the user enters a formula of "MyDoor!Z+10", the "sheet name"
  # in question is "MyDoor". We know that the formula is trying to find the Z
  # value of some part, but to resolve that we must first look at
  # the parts and find one called "MyDoor".
  #
  #   Args:
  #      source_entity: reference to the entity that contained the formula
  #      sheet_name: string name of the "sheet name" we're looking for
  #
  #   Returns:
  #      reference to the found entity or nil is unsuccessful
  def find_entity_by_sheet_name(source_entity, sheet_name)
    sheet_name = sheet_name.downcase

    # First look at the current entity.
    if name_of_entity_is(source_entity, sheet_name)
      return source_entity
    end

    # next look at the parent
    if source_entity.parent
      if !source_entity.parent.to_s.index("Sketchup::Model")
        if source_entity.parent.is_a?(Sketchup::ComponentDefinition)
          parent_entity = source_entity.parent.instances[0]
          if sheet_name.downcase == "parent"
            return parent_entity
          elsif name_of_entity_is(parent_entity, sheet_name, "parent")
            return parent_entity
          end
        end
      end

      # look at the children
      if source_entity.is_a?(Sketchup::ComponentInstance)
        for subentity in source_entity.definition.entities
          if subentity.is_a?(Sketchup::ComponentInstance) ||
             subentity.is_a?(Sketchup::Group)
            if name_of_entity_is(subentity, sheet_name, "children")
              return subentity
            end
          end
        end
      end

      # look at the siblings
      for subentity in source_entity.parent.entities
        if subentity.is_a?(Sketchup::ComponentInstance) || subentity.is_a?(Sketchup::Group)
          if name_of_entity_is(subentity, sheet_name, "sibs")
            return subentity
          end
        end
      end
    end
    # found no matches
    #puts ('found no sheet name on' + sheet_name)
    return nil
  end

  # This method tries to see if a string name from a formula matches the "name"
  # of a given entity. It looks at the stored attribute _name first. If that
  # doesn't match, then it will look at the instance name.
  #
  #   Args:
  #      entity: reference to the entity to check
  #      name: string name we're looking for
  #
  #   Returns:
  #      true or false
  def name_of_entity_is(entity, name, context = "???")
    name = name.downcase.strip

    stored_name = get_attribute_value(entity, "_name")
    stored_name = second_if_empty(stored_name, "").downcase
    if name == stored_name
      return true
    else
      instance_name = copyless_name(entity).strip
      if name == instance_name
        return true
      end
    end

    return false
  end

  # This method strips the " copy N" string off the end of a dynamically copied
  # component and returns the name without that.
  #
  #   Args:
  #      entity: reference to the entity to get the name of
  #
  #   Returns:
  #      name: string of the name
  def copyless_name(entity)
    name = entity.name
    if name.index(" copy ") != nil
      name = name[0..name.index(" copy ") - 1]
    end
    return name.downcase
  end

  # This method parses and returns an attribute's formula result. If no formula
  # is set, it returns nil
  #
  #   Args:
  #      entity: reference to the entity to get the result from
  #      name: string name of the attribute to return the result for
  #
  #   Returns:
  #      value: the formula result of the attribute, or nil if it not set
  def get_formula_result(entity, name, return_error = false)
    formula = get_attribute_formula(entity, name)
    result = nil
    error = nil

    # If the author has exposed even a "live value" attribute to be editable
    # via a text box, then the values entered by an end user will be stored
    # in this @forced_config_values hash. This hash takes precedence over
    # any value or formula for this redraw cycle. This hash will be emptied
    # at the end of the redraw() method.
    entity_dc_name = get_entity_formula_name(entity)
    if get_forced_config_value(entity, name) != nil
      return get_forced_config_value(entity, name)
    end

    # Take a peek at any current error attached to our attribute. If this
    # error changes as the result of running our formula, then we will store
    # that error so it gets displayed in the manager UI.
    current_error = get_attribute_error(entity, name).to_s

    if formula != nil

      # Check for cached results. If found, return those instead of running
      # through the parser.
      formula_key = entity.entityID.to_s + "_" + formula
      if @formula_results[formula_key] != nil
        result = @formula_results[formula_key]
        error = @formula_errors[formula_key]
      else
        result, error = parse_formula(formula, entity, name)
        if error != current_error
          set_attribute_error(entity, name, error)
        end
        # Don't cache formula results with random elements to them, as these
        # you want to rerun every time.
        # TODO(scottlininger): This really isn't smart enough, as you could
        # imagine a scenario where you've cached a formula result that itself
        # refers to a random element. But this is enough of an edge case that
        # we needn't solve it for V1.
        if formula.include?("randbetween") == false &&
           formula.include?("rand") == false
          @formula_results[formula_key] = result
          @formula_errors[formula_key] = error
        end
      end
    else
      # If we have a current_error and we've reached this point, then the user
      # must have deleted the offending formula, so clear the error as well.
      if current_error != ""
        set_attribute_error(entity, name, "")
      end
    end
    if return_error == false
      return result
    else
      return result, error
    end
  end

  # 由于许多属性与 SketchUp 中的实际尺寸相关联，此方法用于返回这些尺寸的实际 “实时值”，而不考虑属性中存储的内容。
  # 例如，某个实体的 “x” 属性可能存储的值为 “25”，但该实体实际的 x 坐标位置是 20。在这种情况下，此方法会返回 20。
  # 参数：entity：要从中获取实时值的实体引用name：要返回其实时值的属性的字符串名称
  # 返回值：value：该属性的实时值；若无效则返回 nil
  def get_live_value(entity, name)
    if entity.is_a?(Sketchup::ComponentDefinition) || entity.is_a?(Sketchup::Model)
      return nil
    end

    name = name.downcase
    if name == "x"
      return entity.transformation.origin.x.to_f
    elsif name == "y"
      return entity.transformation.origin.y.to_f
    elsif name == "z"
      return entity.transformation.origin.z.to_f
    elsif name == "rotx" || name == "roty" || name == "rotz"
      rotations = {}
      rotations["rotx"] = get_formula_result(entity, "rotx")
      rotations["roty"] = get_formula_result(entity, "roty")
      rotations["rotz"] = get_formula_result(entity, "rotz")
      if rotations["rotx"] != nil || rotations["roty"] != nil || rotations["rotz"] != nil
        rotations["rotx"] = second_if_empty(rotations["rotx"], 0.0).to_f
        rotations["roty"] = second_if_empty(rotations["roty"], 0.0).to_f
        rotations["rotz"] = second_if_empty(rotations["rotz"], 0.0).to_f
      else
        rotations["rotx"] = entity.transformation.rotx
        rotations["roty"] = entity.transformation.roty
        rotations["rotz"] = entity.transformation.rotz
      end
      return rotations[name]
    elsif name == "lenx" || name == "leny" || name == "lenz"

      # certain attributes are stored at the instance level of components, so
      # first figure out which entity we are looking at
      if @instance_attributes[name] == true || entity.is_a?(Sketchup::Group)
        attribute_entity = entity
      else
        attribute_entity = entity.definition
      end

      # For components with parts that move themselves, the bounding box
      # lenx, leny, and lenz are not reliable "live values." In such cases,
      # we look for a stored attributes called _ATTNAME_nominal, which stores
      # the nominal value that is reliable. This nominal value is essentially
      # the size that the user *tried* to scale something to.
      #
      # After scaling a dynamic component which has self-positioning parts,
      # the redraw method calls set_nominal_size, which stores these
      # attributes.
      #
      # For example, if I scale a component to be 20" wide, the component
      # might redraw itself to be physically 30" wide. But for purposes of
      # formula calculation, I want to return 20" for my width, or extremely
      # weird scaling bugs appear.
      value = attribute_entity.get_attribute @dictionary_name, "_" + name + "_nominal"
      if value == nil
        sizes = {}
        sizes["lenx"], sizes["leny"], sizes["lenz"] = entity.scaled_size
        value = sizes[name]
      end
      return value
    elsif name == "copy"
      return copy_number_of(entity)
    elsif name == "material"
      material = entity.material
      if material != nil
        # If we have a valid material, then any error can be cleared.
        if get_attribute_error(entity, "material").to_s != ""
          set_attribute_error(entity, "material", nil)
        end

        material_name = material.name
        material_name = material_name.gsub(/^\[/, "")
        material_name = material_name.gsub(/\]$/, "")
        material_name = material_name.gsub(/^\</, "")
        material_name = material_name.gsub(/\>$/, "")
        return material_name
      elsif get_cascading_attribute(entity, "material") != nil
        return get_cascading_attribute(entity, "material")
      else
        # Since this is a "live value", we return an empty string instead of
        # nil, so that formulas that refer to the material won't think that
        # the attribute doesn't exist and therefore throw an error.
        return ""
      end
    else
      return nil
    end
  end

  # Take a string formula and return its complete, parsed value
  #
  # EXAMPLE: a formula such as "MyPart!X+10" might return the x position of a
  # Group called "MyPart" with 10 inches added, for a result of 20.0.
  #
  #   Args:
  #      formula: string formula we're trying to parse
  #      source_entity: optional pointer to the entity that held the formula
  #      source_attribute: optional name of the attribute we're parsing
  #
  #   Returns:
  #      value: the value that was found, or 0.0 if it fails
  #      error: a string containing a marked up version of the formula
  def parse_formula(formula, source_entity = nil, source_attribute = "NAME")
    # Make sure we're handling a string
    formula = formula.to_s

    # Replace any manually escaped quotes (\") with an escaped string.
    # This allows for formulas like ="My name is \"" & NAME & "\""
    formula = formula.gsub(/\\\"/, "%22")

    # Remove any leading or trailing white space
    formula = formula.strip()

    # clean out any inch symbols
    # TODO(scottlininger): We'll really need to handle any escaped (&) entities
    # here for multilingual stuff, since the raw parser right now will see the
    # ampersand as a concatenation operator. So far no problems with that,
    # however.
    formula = formula.gsub(/&quot;/, '"')

    # Escape any characters that are inside double quotes. This is performed
    # with a regular expression that matches anything between two quotes, but
    # only if the first quote is either at the beginning of the string,
    # after an ampersand operator, or after an open parens. For example, the
    # following should match the CAB, but be smart enough to NOT match the 24,
    # since the quote after the 24 denotes a unit of measurement
    #
    # "CAB"&24"
    # 24"&"CAB"
    formula = formula.gsub(/(^|[&\(]?)\"([\s\S]+?)?\"/) { |match|
      leading_ampersand = second_if_empty($1, "")
      quoted_string = $2
      if quoted_string != nil
        quoted_string = quoted_string.gsub(/\%22/, '"')
        quoted_string = escape(quoted_string, false)
      else
        quoted_string = ""
      end
      leading_ampersand + '"' + quoted_string + '"'
    }

    # Replace spaces around operators
    formula = formula.gsub(/\s*([\*\/\+\-\=\>\<\\(\)\&,\!])\s*/, '\1')

    # If our formula begins with a '-', then insert a 0 to do math against.
    # This solves cases like -(-10).
    formula = formula.gsub(/^\-/, "0-")

    # Replace minus operators with +- to handle negative numbers, but only in
    # the places where the '-' is truly an operator and not just a negative
    # sign. (For example, 5<-5 should not be turned into 5<+-5, but 5-5 should
    # be turned into 5+-5.
    formula = formula.gsub(/([^\*\/\+\=\>\<\\(\&\!]{1})\-(.)/) { |match|
      leading_char = second_if_empty($1, "")
      trailing_char = second_if_empty($2, "")
      if leading_char == ")"
        leading_char = ")+"
      elsif leading_char != ""
        leading_char += "+"
      end
      if trailing_char == "("
        leading_char += "+"
      elsif trailing_char.index(/[a-zA-Z]/) == 0
        trailing_char = "1*" + trailing_char
      end
      leading_char + "-" + trailing_char
    }

    formula = formula.gsub(/\$/, "")
    formula = formula.gsub(/\'/, "")

    # If we are parsing in context of a SketchUp entity, then handle circular
    # references in formulas here. We must do this *after* we've made the above
    # string adjustments to the formula so that our reference_list hash will
    # hit matches properly, since the formula is part of the key.
    if source_entity

      # Assume for the moment that this is being called from within
      # parse_reference, meaning that it is not the responsibility of this
      # method call to clear out the master "clean or dirty" reference list
      should_clear_reference_list = false

      # If a @reference_list hash does not exist, then we know that this is
      # not being called inside of parse_reference, meaning this is a "top
      # level" formula and we need to keep track of all references that
      # we parse from here on out. Therefore, create the hash and set
      # should_clear_reference_list to true, so we remember to wipe it clean
      # when we reach the end of our method call
      if @reference_list == nil
        should_clear_reference_list = true
        @reference_list = {}
      end

      # If @reference_list contains a key match that says we've come here
      # before, then we've encountered a circular reference and need to bail
      # out with an error.
      #
      # If it doesn't contain a key match, then instead add a key that says
      # that we've been here.
      if @reference_list[source_entity.entityID.to_s + "_" +
                           formula.to_s] == true
        if should_clear_reference_list == true
          @reference_list = nil
        end
        return nil, "<span class=subformula-error>" +
                    translate("Circular Reference") + "</span>"
      else
        @reference_list[source_entity.entityID.to_s + "_" +
                          formula.to_s] = true
      end
    end

    result, error = parse_subformula(formula, source_entity, source_attribute)

    # Once we've completed our formula parsing, then here's where we clear
    # out the @reference_list appropriately
    if source_entity
      @reference_list[source_entity.entityID.to_s + "_" + formula.to_s] = false
      if should_clear_reference_list == true
        @reference_list = nil
      end
    end

    # If an error was encountered along the way, then return nil
    if error.to_s.include? "subformula-error"
      return nil, error
    elsif result != nil

      # Figure out which units this formula is being processed under.
      source_units = get_attribute_formulaunits(source_entity,
                                                source_attribute, true)
      result = @conv.to_base(result, source_units)

      # If we have a string result, unencode it to display any special
      # characters properly, otherwise return the raw value
      if result.kind_of? String
        return unescape(result), error
      else
        if result.kind_of? Float
          if result.to_s.include? "e-"
            result = 0.0
          end
        end

        return result, error
      end
    else
      return nil, ""
    end
  end

  # Take a smaller chunk of a full string formula and return its complete,
  # parsed value. This is a recursive function that breaks up a formula into
  # smaller and smaller subformulas until "atomic" units are reached and
  # passed back up the tree.
  #
  # EXAMPLE: a formula such as "MyPart!X+10" would be split into two subformulas
  # and parse_subformula("MyPart!X") and parse_subformula("10") would be
  # recursively called.
  #
  #   Args:
  #      formula: string formula we're trying to parse
  #      source_entity: optional pointer to the entity that held the formula
  #      source_entity: optional name of the attribute we're parsing for
  #
  #   Returns:
  #      value: the value that was found, or 0.0 if it fails
  #      error: a string containing a marked up version of the formula
  def parse_subformula(formula, source_entity = nil, source_attribute = nil)
    # Replace extra negatives with a simpler expression.
    formula = formula.gsub(/\-\-/, "+")
    formula = formula.gsub(/\-\+/, "-")

    # If there are no operators, then return the value of the formula.
    if formula.index(/[\*\/\+\=\>\<\\(\)\&]/) == nil
      # If there are "word" characters, parse for referenced attribute's value.
      if formula.index(/[A-Za-z,\s]/)
        return parse_reference(source_entity, formula, source_attribute)
      else
        if formula.kind_of? String
          formula = chop_quotes(formula)
        end
        return formula, formula
      end
    elsif formula.index(/[\(\)]/) # handle parentheses and function calls
      new_formula = ""
      error = ""
      function = nil
      inside_parens_formula = ""
      opening_parens_count = 0
      # look at each character searching for opening and closing parens
      for i in 0..(formula.length - 1)
        char = formula[i..i]
        if opening_parens_count > 1 && char == ","
          # If the inside_parens_formula ends up being part of a function
          # call, then we will take that inside_parens_formula and split
          # it along the commas so we can pass multiple parameters to
          # our actual function. The problem is that if we are inside a
          # nested formula call, we do NOT want to split on the commas
          # inside the inner parenthesis, so we mst replace the comma
          # with an escaped version so we know not to split on it.
          #
          # Example: the call  IF(true,A, IF(true, B, C) ) will end up
          # being marked up as IF(true,A, IF(true%2C B%2C C) )
          inside_parens_formula = inside_parens_formula + "%2C"
        elsif opening_parens_count > 0
          if char == "("
            opening_parens_count = opening_parens_count + 1
            if opening_parens_count > 1
              inside_parens_formula = inside_parens_formula + char
            end
          elsif char == ")"
            if opening_parens_count > 1
              inside_parens_formula = inside_parens_formula + char
            end
            opening_parens_count = opening_parens_count - 1
          else
            inside_parens_formula = inside_parens_formula + char
          end
          if opening_parens_count == 0
            possible_func_start = second_if_empty(new_formula.rindex(/\W/), -1) + 1
            possible_func = new_formula[possible_func_start..new_formula.length]

            if possible_func != ""
              possible_func = possible_func.downcase

              if @parser_functions.is_supported(possible_func)
                error = error + possible_func + "("
                if possible_func_start == 0
                  new_formula = new_formula[new_formula.length..9999]
                else
                  new_formula = new_formula[0..possible_func_start - 1] + new_formula[new_formula.length..9999]
                end
                parsed_params = []
                params = inside_parens_formula.split(/,/)
                for param in params
                  # In a earlier part of this routine where we were parsing
                  # through the contents of this formula result, we marked
                  # up any commas with %2C so that we could differentiate
                  # between commas inside nested parens.
                  param = param.gsub(/%2C/, ",")
                  parsed_param, param_error = parse_subformula(param,
                                                               source_entity, source_attribute)
                  parsed_params.push(parsed_param)
                  # Append a comma to our error string only if this isn't the
                  # first parameter in the list. One way to know this is if the
                  # last character in the string is a '('
                  if error[error.length - 1..error.length - 1] != "("
                    error = error + ","
                  end
                  error = error + param_error.to_s
                end
                begin
                  inside_parens_result, suberror = @parser_functions.call(possible_func, parsed_params, source_entity, self)
                rescue => exception
                  inside_parens_result = false
                  suberror = "<span class=subformula-error>[#{exception}]</span> "
                end
              else
                suberror = '<span class="subformula-error">' +
                           translate("UNKNOWN FUNCTION:") + " " + possible_func +
                           "</span>(" + inside_parens_formula
              end
            else
              error = error + "("
              # In a earlier part of this routine where we were parsing
              # through the contents of this formula result, we marked
              # up any commas with %2C so that we could differentiate
              # between commas inside nested parens.
              inside_parens_formula = inside_parens_formula.gsub(/%2C/, ",")
              inside_parens_result, suberror = parse_subformula(
                inside_parens_formula, source_entity, source_attribute
              )
            end

            if inside_parens_result.kind_of? String
              # If we have a string that is actually a number, then let's go
              # ahead and return the number.
              if inside_parens_result.to_f.to_s != inside_parens_result and
                 inside_parens_result.to_i.to_s != inside_parens_result
                inside_parens_result = '"' + inside_parens_result + '"'
              end
            else
              inside_parens_result = inside_parens_result.to_s
              if inside_parens_result.index(/e-\d\d\d/) ==
                 inside_parens_result.length - 5
                inside_parens_result = "0.0"
              end
            end

            new_formula = new_formula + inside_parens_result.to_s
            error += suberror.to_s + ")"
            inside_parens_formula = ""
          end
        else
          if char == "("
            opening_parens_count = opening_parens_count + 1
          elsif char == ")"
            opening_parens_count = opening_parens_count - 1
          else
            new_formula = new_formula + char
          end
        end
      end
      if opening_parens_count != 0
        error = "<span class=subformula-error>" + translate("PARENS COUNT") +
                "</span> "
      end
      if error.include? "subformula-error"
        return new_formula, error
      else
        return parse_subformula(new_formula.to_s, source_entity,
                                source_attribute)
      end
    elsif formula.index(/[\=\>\<]/) # handle comparison
      # to ease the regular expression match, replace <> with =!
      formula = formula.gsub(/\<\>/, "=!")

      # Quotes and greaterthan/lessthan symbols do not register as
      # a non-word break when next to one another, so insert a space
      # to handle those scenarios
      formula = formula.gsub(/\"\</, '" <')
      formula = formula.gsub(/\"\>/, '" >')
      formula = formula.gsub(/\"\=/, '" =')

      operands = formula.split(/(\b|\s)(?=[\=\>\<])/)
      result = nil # create a variable outside the each_index block
      error = ""

      # If we've reached this far and have only one operand, then they have
      # a malformed compare such as ';<;', in which case throw an error
      # rather than crashing.
      if operands.length < 2
        return 0.0, "<span class=subformula-error>" +
                    translate("COMPARISON ERROR") + "</span> "
      end

      operands.each_index { |i|
        if operands[i].length > 0 && operands[i] != " "

          # undo our =! match from above with the spreadsheet-style <>
          this_operand = operands[i].gsub(/\=\!/, "<>")
          operator, clean_operand = strip_operator(this_operand, "")
          parsed_operand, suberror = parse_subformula(clean_operand,
                                                      source_entity, source_attribute)
          error = error + operator + suberror

          if result == nil
            parsed_operand = parsed_operand.to_s
          elsif result.kind_of? String
            # Strings that are integers or floats should match, so check for
            # that here. "1" and 1.0, "1.0" and 1.0, and "1" and 1 should
            # match, for purposes of comparison. This is to be
            # consistent with Google Spreadsheets.
            if result.to_i.to_s == result || result.to_f.to_s == result
              parsed_operand = parsed_operand.to_f
              result = result.to_f
            else
              parsed_operand = parsed_operand.to_s
            end
          else
            parsed_operand = parsed_operand.to_f
          end

          # The first parsed_operand in the list becomes our initial result
          if result == nil
            result = parsed_operand
          elsif operator == "<"
            if result < parsed_operand
              result = 1.0
            else
              result = 0.0
            end
          elsif operator == ">"
            if result > parsed_operand
              result = 1.0
            else
              result = 0.0
            end
          elsif operator == ">="
            if result >= parsed_operand
              result = 1.0
            else
              result = 0.0
            end
          elsif operator == "<="
            if result <= parsed_operand
              result = 1.0
            else
              result = 0.0
            end
          elsif operator == "<>"
            if result != parsed_operand
              result = 1.0
            else
              result = 0.0
            end
          elsif operator == "="
            # TODO:(scottlininger) string comparison as well as numeric
            if result == parsed_operand
              result = 1.0
            else
              result = 0.0
            end
          end
        end
      }
      return result, error
    elsif formula.index(/\&/) # handle concatenation
      result = ""
      error = ""
      operator = "&"
      operands = formula.split(/\&/)
      for operand in operands
        operator, clean_operand = strip_operator(operand, "&")
        parsed_operand, suberror = parse_subformula(clean_operand,
                                                    source_entity, source_attribute)
        parsed_operand = @conv.clean_number(parsed_operand)
        result = result + parsed_operand.to_s
        if error == ""
          error = suberror
        else
          error = error + operator + suberror
        end
      end
      return result, error
    elsif formula.index(/[\+]/) # handle addition
      result = 0.0
      error = ""
      operands = formula.split(/(\b|\B)(?=[\+])/)
      for operand in operands
        if operand.length > 0
          operator, clean_operand = strip_operator(operand, "+")
          parsed_operand, suberror = parse_subformula(clean_operand,
                                                      source_entity, source_attribute)
          result = result + parsed_operand.to_f
          if error == ""
            error = suberror
          else
            error = error + operator + suberror
          end
        end
      end
      return result, error
    elsif formula.index(/[\*\/]/) # handle multiplication and division
      result = 1.0
      error = ""
      operands = formula.split(/(\b|\B)(?=[\*\/])/)
      for operand in operands
        if operand.length > 0
          operator, clean_operand = strip_operator(operand, "*")
          parsed_operand, suberror = parse_subformula(clean_operand,
                                                      source_entity, source_attribute)
          if operator == "*"
            if parsed_operand.to_s.length != 0
              result = result * parsed_operand.to_f
            end
          else
            if parsed_operand.to_f == 0.0
              result = result
              error = "<span class=subformula-error>" + translate("DIV/0!") +
                      "</span> "
            else
              result = result / parsed_operand.to_f
            end
          end
          if error == ""
            error = suberror
          else
            error = error + operator + suberror
          end
        end
      end
      return result, error
    end
  end

  # Takes a string that is a potential subformula, such as "+25" and returns
  # the operators (+) and operand (25).
  #
  #   Args:
  #      operand: the string we're trying to strip
  #      default_operator: in certain contexts, there is an implied operator
  #                        that will be returned if an explicit one isn't found
  #
  #   Returns:
  #      operator: string of the first operator found
  #      operand: string of the remaining string
  def strip_operator(operand, default_operator)
    first_char = operand[0..0]
    if first_char == "*" || first_char == "/" || first_char == "+" ||
       first_char == "=" || first_char == "&"
      return first_char, operand[1..9999]
    elsif first_char == "<" || first_char == ">"
      second_char = operand[1..1]
      if second_char == "=" || (first_char == "<" && second_char == ">")
        return first_char + second_char, operand[2..9999]
      else
        return first_char, operand[1..9999]
      end
    else
      return default_operator, operand
    end
  end

  # Looks at an entity and returns true if that entity has any attributes
  # that an be viewed or configured by the consumer.
  #
  #   Args:
  #      entity: reference to the entity in question
  #
  #   Returns:
  #      boolean: true or false depending on the attributes of the entity
  def can_be_configured(entity)
    if is_3d_warehouse(entity)
      entity = entity.definition.entities[0]
    end

    # TODO(scottlininger): Instead of hard coding the list of meta attributes,
    # we should call back to our reserved library
    for attribute_name in get_attribute_list(entity)
      if attribute_name == "name" || attribute_name == "summary" ||
         attribute_name == "description" || attribute_name == "msrp" ||
         (get_attribute_access(entity, attribute_name) != nil &&
          get_attribute_access(entity, attribute_name) != "NONE")
        return true
      end
    end
    # Currently we are not supporting "deep" configurations - all configurable
    # options must be set at the top level, so this code is likely to go away
    #
    #if entity.typename == "ComponentInstance"
    #  subentities = entity.definition.entities
    #elsif entity.typename == "Group"
    #  subentities = entity.entities
    #end
    #
    #for subentity in subentities
    #  if subentity.typename=="Group" || subentity.typename=="ComponentInstance"
    #    if can_be_configured(subentity) == true
    #      return true
    #    end
    #  end
    #end
    return false
  end

  # When you insert a 3D warehouse or SketchUp Dynamic Component in your
  # drawing, they come in with a "wrapper" component that contains nothing but
  # your Dynamic Component. This method looks for those situations and
  # explodes them when it finds one, which eliminates some bugs and confusion
  # that could occur.
  #
  #   Args:
  #      instance: reference to the entity in question
  #
  #   Returns:
  #      the exploded instance
  def explode_if_3d_warehouse(instance)
    # TODO(scottlininger): This is a temporary comment to disable the silent
    # explode of DCs. We can clean up all calls to this method and get rid of
    # it if we don't see any regressions from this.
    #if is_3d_warehouse(instance)
    #  entities = instance.explode
    #  for entity in entities
    #    if entity.typename == "ComponentInstance"
    #      Sketchup.active_model.selection.add(entity)
    #      return entity
    #    end
    #  end
    #end
    return instance
  end

  # When you insert a 3D warehouse or SketchUp Dynamic Component in your
  # drawing, they come in with a "wrapper" component that contains nothing but
  # your Dynamic Component. This method looks for this situation and
  # returns true if it finds one.
  #
  #   Args:
  #      instance: reference to the entity in question
  #
  #   Returns:
  #      true or false
  def is_3d_warehouse(instance)
    if instance.is_a?(Sketchup::ComponentInstance)
      instance_name = get_attribute_value(instance, "_name")
      if instance_name == nil
        subentity = instance.definition.entities[0]
        if (subentity.is_a?(Sketchup::ComponentInstance) &&
            instance.definition.entities.length == 1)
          if has_behaviors(subentity)
            return true
          end
        end
      end
    end
    return false
  end

  # When a dynamic component changes its own geometry, a new, unique definition
  # must be created so the user sees what they would expect. Otherwise changing
  # one dynamic component instance would alter all of the others in your model.
  #
  #   Args:
  #      instance: reference to the entity in question
  #
  #   Returns:
  #      Nothing
  def make_unique_if_needed(instance)
    if instance.is_a?(Sketchup::ComponentInstance)
      if (instance.definition.count_instances > 1)
        if children_have_behaviors(instance)
          instance.make_unique
        end
      end
    end
  end

  # Corrects for floating point errors by rounding to 7 decimal places
  #
  #   Args:
  #     f: a float
  #
  #   Returns:
  #     float: f rounded to 7 decimal places
  def fix_float(f)
    return ((f.to_f * 10000000.0).round / 10000000.0)
  end

  def chop_quotes(reference)
    if (reference[0..0] == '"') &&
       (reference[reference.length - 1..reference.length - 1] == '"')
      return reference[1..reference.length - 2]
    else
      return reference
    end
  end

  # Updates our open dialogs.
  def refresh_dialogs(selection_just_changed = false)
    if selection_just_changed == true
      clear_highlight()
    end

    refresh_manager_dialog()
    refresh_configure_dialog()
  end

  # Returns true if the configure dialog is currently visible.
  def configure_dialog_is_visible()
    # puts "configure_dialog_is_visible   ……ing👇"
    dialog_name = $dc_CONFIGURATOR_NAME
    if @dialogs[dialog_name] != nil
      if @dialogs[dialog_name].visible?
        return true
      end
    end
    return false
  end

  # Returns true if the configure dialog is currently visible.
  def reporter_dialog_is_visible()
    dialog_name = $dc_REPORTER_NAME
    if @dialogs[dialog_name] != nil
      if @dialogs[dialog_name].visible?
        return true
      end
    end
    return false
  end

  # Returns true if the manager dialog is currently visible.
  def manager_dialog_is_visible()
    dialog_name = $dc_MANAGER_NAME
    if @dialogs[dialog_name] != nil
      if @dialogs[dialog_name].visible?
        return true
      end
    end
    return false
  end

  # Closes the configure dialog.
  def close_configure_dialog()
    dialog_name = $dc_CONFIGURATOR_NAME
    if @dialogs[dialog_name] != nil
      @dialogs[dialog_name].close()
    end
  end

  # Gets a reference to the configure dialog.
  def get_configure_dialog()
    dialog_name = $dc_CONFIGURATOR_NAME
    return @dialogs[dialog_name]
  end

  # Closes the configure dialog.
  def close_reporter_dialog()
    dialog_name = $dc_REPORTER_NAME
    if @dialogs[dialog_name] != nil
      @dialogs[dialog_name].close()
    end
  end

  # Closes the manager dialog.
  def close_manager_dialog()
    dialog_name = $dc_MANAGER_NAME
    if @dialogs[dialog_name] != nil
      if @dialogs[dialog_name].visible?
        @dialogs[dialog_name].close()
      end
    end
  end

  # If the configure dialog is currently visible, it forces a refresh by
  # calling cfg.handlePullSelectionIdsComplete()
  def refresh_configure_dialog()
    #puts 'refresh_configure_dialog'
    dialog_name = $dc_CONFIGURATOR_NAME
    if @dialogs[dialog_name] != nil
      if @dialogs[dialog_name].visible?
        ids = ""
        if Sketchup.active_model
          for entity in Sketchup.active_model.selection
            if entity.is_a?(Sketchup::ComponentInstance) ||
               entity.is_a?(Sketchup::Group)

              # Do not add to the configure list any components that
              # have a versioning error. (Meaning they are a version of
              # DCs that this plugin does not know how to handle.)
              if @dcobservers.has_version_error(entity) == false
                ids = ids + entity.entityID.to_s + ","
                @entity_pointers[entity.entityID.to_s] = entity
              end
            end
          end
          save_configure_thumbnail()
        end

        ids = ids.gsub(/,$/, "")
        ids = (ids == "") ? "-1" : ids
        command = 'cfg.handlePullSelectionIdsComplete("","' + ids + '")'
        @dialogs[dialog_name].execute_script(command)
      end
    end
  end

  # Saves out a thumbnail PNG of the first selected entity to a standard
  # location that the configure dialog will show.
  def save_configure_thumbnail()
    if Sketchup.active_model.selection.length > 0
      entity = Sketchup.active_model.selection[0]
      if entity.is_a?(Sketchup::ComponentInstance)
        temp_thumbnail_file = "#{get_temp_path}/config-thumb.png"
        success = entity.definition.save_thumbnail(temp_thumbnail_file)
      end
    end
  end

  # Refreshes the manager dialog if it is open.
  def refresh_manager_dialog()
    dialog_name = $dc_MANAGER_NAME
    dialog = @dialogs[dialog_name]
    if dialog != nil
      if dialog.visible?
        dialog.execute_script("mgr.doRefresh()")
      end
    end
    return false
  end

  # Replaces the entity passed with a skp that the user selects from their
  # harddrive, and places any attributes that overlap into the new one.
  #
  def swap_component(old_instance)
    old_instance = explode_if_3d_warehouse(old_instance)
    if @swap_path == nil
      @swap_path = Sketchup.find_support_file("Components")
    end

    path = UI.openpanel translate("Select a SKP"), @swap_path, "*.skp"
    if path

      # First, store the path the user was last browsing so we can return them
      # to that folder the next time they run thie swap_component method
      @swap_path = path

      current_transform = old_instance.transformation

      model = Sketchup.active_model
      begin
        definition = model.definitions.load path
      rescue
        UI.messagebox(translate("That does not appear to be a valid SketchUp " +
                                "file. Please select a different one."))
        return
      end
      Sketchup.active_model.start_operation translate("Swap Component"), true

      if Sketchup.active_model.active_path == nil
        target_entities = model.entities
      else
        context_entity = Sketchup.active_model.active_path.pop
        if context_entity.is_a?(Sketchup::Group)
          target_entities = context_entity.entities
        else
          target_entities = context_entity.definition.entities
        end
      end

      new_instance = target_entities.add_instance definition, current_transform
      new_instance = explode_if_3d_warehouse(new_instance)

      # Loop across the old attributes, and apply any that exist on the new
      # instance. (We only want to overwrite attributes that match between old
      # and new.)
      old_list = get_attribute_list(old_instance)
      for name in old_list
        value = get_attribute_value(old_instance, name)
        default_value = get_attribute_value(new_instance, name)
        if default_value != nil
          set_attribute(new_instance, name, value)
        end
      end

      status = Sketchup.active_model.selection.add new_instance
      old_instance.erase!

      DCProgressBar::clear()
      redraw(new_instance)
      DCProgressBar::clear()
      Sketchup.active_model.commit_operation
      refresh_dialogs()
    end
  end

  # Returns true or false depending on whether the user is a free user.
  def is_free?
    return !Sketchup.is_pro?
  end

  # The highlight instance is a component which is nested inside other
  # components to indicate that they are selected inside the manager. This
  # method not only destroys this instance inside SketchUp, but it also sends
  # a command to the manager panel to clear its own highlight.
  def clear_highlight()
    dialog_name = $dc_MANAGER_NAME
    if @dialogs[dialog_name] != nil
      if @dialogs[dialog_name].visible?
        command = "mgr.clearSelection()"
        @dialogs[dialog_name].execute_script(command)
      end
    end
  end

  # Erases the highlight instance from the model.
  def destroy_highlight_instance()
    if @highlight != nil
      if @highlight.valid?
        @highlight.erase!
      end
    end
  end

  # Reestablishes the last highlight by calling mgr.resetLastSelection which
  # in turn will call push_selection handler to complete the reset loop. This
  # method is called when one enters or leaves edit mode to ensure that the
  # highlight instance is drawn into the correct spot. (It is never drawn into
  # the current context, but always into the model context or a subcomponent
  # context.)
  def reset_last_highlight()
    dialog_name = $dc_MANAGER_NAME
    if @dialogs[dialog_name] != nil
      if @dialogs[dialog_name].visible?
        command = "mgr.resetLastSelection();"
        @dialogs[dialog_name].execute_script(command)
      end
    end
  end

  # Returns a nicely formatted copy name. For example, the 3rd copy of an entity
  # named "Board" would return "Board copy 003"
  #
  #   Args:
  #      base_name(string)     String name of the base entity
  #      copy_number(integer)  Integer number of the copy
  #
  #   Returns:
  #      (string) nicely formatted name
  def name_of_copy(base_name, copy_number)
    return base_name.to_s + " copy " + copy_number.to_i.to_s.rjust(3, "0")
  end

  # Checks a DC that has just been placed into the model to determine whether
  # a material application or a redraw is needed.
  #
  #   Args:
  #      entity(entity)     The component instance that just was placed.
  #
  #   Returns:
  #      nothing
  def handle_place_component(entity)
    if entity.is_a?(Sketchup::ComponentInstance)
      if has_instance_cache(entity) || has_movetool_behaviors(entity) ||
         get_attribute_formula(entity, "material") != nil ||
         get_attribute_formula(entity, "hidden") != nil ||
         (children_have_behaviors(entity) == false &&
          (get_attribute_formula(entity, "lenx") != nil ||
           get_attribute_formula(entity, "leny") != nil ||
           get_attribute_formula(entity, "lenz") != nil))
        Sketchup.active_model.start_operation translate("Component"),
          true, false, true
        apply_instance_cache(entity)
        lenx, leny, lenz = entity.scaled_size
        entity.set_last_size(lenx, leny, lenz)
        determine_movetool_behaviors(entity)
        DCProgressBar::clear()
        redraw(entity)
        DCProgressBar::clear()
        refresh_dialogs()
        Sketchup.active_model.commit_operation
      end
    end
  end

  # Returns an array of instance cache attribute names, which are attributes
  # with names like "_inst_x".
  #
  #   Args:
  #      entity(entity)     The entity that we want any cached instance names.
  #
  #   Returns:
  #      array of attribute names that are instance cache names.
  def get_instance_cache_list(entity)
    list = []
    if entity.is_a?(Sketchup::ComponentInstance)
      attribute_entity = entity.definition
      if attribute_entity.attribute_dictionaries
        if attribute_entity.attribute_dictionaries[@dictionary_name]
          dictionary = attribute_entity.attribute_dictionaries[@dictionary_name]
          for key in dictionary.keys
            if key[0..@instance_cache_prefix.length - 1] == @instance_cache_prefix
              list.push key
            end
          end
        end
      end
    end
    return list
  end

  # Returns an array of instance attributes in the DC dictionary.
  #
  #   Args:
  #      entity(entity)     The entity that we want instance attribute names.
  #
  #   Returns:
  #      array of attribute names that are instance attribute names.
  def get_instance_attribute_list(entity)
    list = []
    if entity.is_a?(Sketchup::ComponentInstance)
      attribute_entity = entity
      if attribute_entity.attribute_dictionaries
        if attribute_entity.attribute_dictionaries[@dictionary_name]
          dictionary = attribute_entity.attribute_dictionaries[@dictionary_name]
          for key in dictionary.keys
            list.push key
          end
        end
      end
    end
    return list
  end

  # Accepts an entity, an attribute name, and a value that has been pushed down
  # from one of our dialogs. It then checks to see if the value begins with an
  # asterisk and is an autocad-style relative value, such as '@+5' or '@-12'. If
  # so, it will apply that change from the live value and return the adjusted
  # value. Otherwise it just returns the original value.
  #
  #   Args:
  #      entity(entity)     The entity that this attribute is applying to
  #      name(string)       The name of the attribute to check
  #      value(string)      The value that was pushed down.
  #
  #   Returns:
  #      the corrected value
  def apply_relative_move(entity, name, value)
    # For positioning variables, @ sign as the first character will apply
    # a "relative move"
    if name == "x" || name == "y" || name == "z" || name == "rotx" ||
       name == "roty" || name == "rotz" || name == "lenx" ||
       name == "leny" || name == "lenz"
      if value.to_s[0..0] == "@"
        value = value.to_s[1..999].to_f
        value = value + get_live_value(entity, name)
      end
    end
    return value
  end

  # Accepts an entity and an attribute name and returns any forced configuration
  # value as earlier set by set_forced_configuration_value(). Returns nil if
  # nothing is found.
  #
  #   Args:
  #      entity(entity)     The entity that this attribute is applying to
  #      name(string)       The name of the attribute to check
  #
  #   Returns:
  #      the corrected value
  def get_forced_config_value(entity, name)
    key = entity.entityID.to_s + "_" + name.to_s
    return @forced_config_values[key]
  end

  # Accepts an entity, an attribute name, and a value and stores a hash entry
  # that can later be retrieved by get_forced_value().
  #
  #   Args:
  #      entity(entity)     The entity that this attribute is applying to
  #      name(string)       The name of the attribute to check
  #      value(variant)     The value to store
  #
  #   Returns:
  #      Nothing
  def set_forced_config_value(entity, name, value)
    key = entity.entityID.to_s + "_" + name.to_s
    @forced_config_values[key] = value
  end

  # Accepts an optional entity and wipes out any forced config values attached
  # to it. If you do not pass an entity, it wipes them all out.
  #
  #   Args:
  #      entity(entity)     Optional entity to clear the values from
  #
  #   Returns:
  #      Nothing
  def clear_forced_config_values(entity = nil)
    if entity == nil
      @forced_config_values.clear
      return
    end
    for key in @forced_config_values.keys
      entity_id = entity.entityID.to_s
      if key.index(entity_id + "_") == 0
        @forced_config_values.delete(key)
      end
    end
  end

  # Returns true if the entity has any forced config values.
  #
  #   Args:
  #      entity(entity)     Optional entity to clear the values from
  #
  #   Returns:
  #      true or false
  def has_forced_config_values(entity)
    for key in @forced_config_values.keys
      entity_id = entity.entityID.to_s
      if key.index(entity_id + "_") == 0
        return true
      end
    end
    return false
  end

  # Force refresh of thumbnail of a component and its parents, recursively.
  #
  #   Args:
  #      entity(entity)     Component Instance to refresh the thumbnail for.
  #
  #   Returns:
  #      nothing
  def refresh_thumbnails(entity)
    if entity.is_a?(Sketchup::ComponentInstance)
      entity.definition.refresh_thumbnail
      refresh_thumbnails(entity.parent)
    elsif entity.is_a?(Sketchup::ComponentDefinition)
      entity.refresh_thumbnail
      for instance in entity.instances
        refresh_thumbnails(instance.parent)
      end
    elsif entity.is_a?(Sketchup::Group)
      refresh_thumbnails(entity.parent)
    end
  end

  # Return the units string based on the user's current default settings.
  #
  #   Args:
  #      none
  #
  #   Returns:
  #      unit string, such as INCHES, FEET, etc.
  def get_default_units
    length = Sketchup.format_length 10
    if length =~ /mm/
      return "MILLIMETERS"
    elsif length =~ /cm/
      return "CENTIMETERS"
    elsif length =~ /m/
      return "METERS"
    elsif length =~ /\'/
      return "FEET"
    else
      return "INCHES"
    end
  end

  # Return the delimiter used in decimal points.
  #
  #   Args:
  #      none
  #
  #   Returns:
  #      delimiter string, either '.' or ','
  def get_decimal_delimiter
    angle = Sketchup.format_angle 1.1
    if angle =~ /,/
      return ","
    else
      return "."
    end
  end

  # Return the units string based on the user's current default settings.
  # TODO(scottlininger): For v1, we are only supporting cm and inches, though
  # this could easily be expanded down the road.
  #
  #   Args:
  #      none
  #
  #   Returns:
  #      unit string, such as INCHES, FEET, etc.
  def get_default_authoring_units
    units = get_default_units
    if units == "FEET"
      units = "INCHES"
    elsif units != "INCHES" && units != "CENTIMETERS"
      units = "CENTIMETERS"
    end
    return units
  end

  # Returns the path to the system's temp directory.
  #
  #   Args:
  #      none
  #
  #   Returns:
  #      string
  def get_temp_path
    # Check if SU 2014+ and use the new method if possible.
    # Getting the env strings returns ASCII encoded strings in Ruby 2.0 and
    # causes unicode issues.
    return Sketchup::temp_dir if defined?(Sketchup::temp_dir)

    %w{TMPDIR TMP TEMP}.each { |key|
      path = ENV[key]
      if path && File.directory?(path) && File.writable?(path)
        return path
      end
    }
    puts "No writable temp folder found!"
    return ""
  end

  # Returns whether an entity has any dc attributes at all.
  #
  #   Args:
  #      none
  #
  #   Returns:
  #      true or false
  def has_dc_attribute_dictionary(entity)
    if entity.attribute_dictionaries
      if entity.attribute_dictionaries[@dictionary_name]
        return true
      end
    end
    if entity.is_a?(Sketchup::ComponentInstance)
      if entity.definition.attribute_dictionaries
        if entity.definition.attribute_dictionaries[@dictionary_name]
          return true
        end
      end
    end
    return false
  end

  # Converts base value from one attribute into another, if needed.
  #
  #   Args:
  #      value  the value to convert
  #      source_entity  the entity we're converting from
  #      source_attribute  the attribute we're converting from
  #      target_entity  the entity we're converting to
  #      target_attribute  the attribute we're converting to
  #
  #   Returns:
  #      The value contained in "attribute" converted into the appropriate
  #        units for storage in "target_attribute"
  def convert(value, source_entity, source_attribute, target_entity,
              target_attribute)
    source_units = get_attribute_formulaunits(source_entity,
                                              source_attribute, true)
    source_group = @conv.units_hash[source_units]["group"]
    target_units = get_attribute_formulaunits(target_entity,
                                              target_attribute, true)
    target_group = @conv.units_hash[target_units]["group"]
    if target_group == source_group
      return @conv.from_base(value, target_units)
    else
      return @conv.from_base(value, source_units)
    end
  end

  # Since the scale tool observers do not return a simple scale factor that was
  # applied, we keep track of the *last* size of any given DC, so that on
  # subsequent scales, we can compare new size to last and figure out what the
  # user did. The interact tool has the potential to alter these last sizes
  # by animating a subcomponent, so this method is called at the end of an
  # animation so we can get those sizes updated. It updates the last size of
  # a given entity and all of that entity's parents.
  #   Args:
  #      entity  the entity to update
  def update_last_sizes(entity)
    parent = entity.parent
    if parent
      if has_dc_attribute_dictionary(parent)
        if parent.is_a?(Sketchup::ComponentDefinition)
          # Make sure that the reported bounding box is up to date.
          parent.invalidate_bounds
          for instance in parent.instances
            lenx, leny, lenz = instance.scaled_size
            instance.set_last_size(lenx, leny, lenz)
            update_last_sizes(instance)
          end
        elsif parent.is_a?(Sketchup::Group)
          lenx, leny, lenz = parent.scaled_size
          parent.set_last_size(lenx, leny, lenz)
          update_last_sizes(parent)
        end
      end
    end
  end

  # Accessor for @is_mac
  def is_mac
    @is_mac
  end
end
